gramex.services.emailer

SMTPMailer(type, email=None, password=None, host=None, port=None, tls=True, stub=None)

Creates an object capable of sending HTML emails.

Examples:

>>> mailer = SMTPMailer(type='gmail', email='gramex.guide@gmail.com', password='...')
>>> mailer.mail(
... to='person@example.com',
... subject='Subject',
... html='<strong>Bold text</strong>. <img src="cid:logo">'
... body='This plain text is shown if the client cannot render HTML',
... attachments=['1.pdf', '2.txt'],
... images={'logo': '/path/to/logo.png'})

Parameters:

Name Type Description Default
type str

Email service type

required
email str

SMTP server login email ID

None
password str

SMTP server login email password

None
host str

SMTP server. Not required when a type is specified

None
port int

SMTP server port. Defaults to 25 for non-TLS, 587 for TLS

None
tls bool

True to use TLS, False to use non-TLS

True
stub str

‘log’ prints email contents instead of sending it

None

type can be:

  • gmail: Google Mail (smtp.gmail.com)
  • yahoo: Yahoo Mail (smtp.mail.yahoo.com)
  • live: Live.com mail (smtp.live.com)
  • mandrill: Mandrill (smtp.mandrillapp.com)
  • office365: Office 365 (smtp.office365.com)
  • outlook: Outlook (smtp-mail.outlook.com)
  • icloud: Apple iCloud (smtp.mail.me.com)
  • mail.com: Mail.com (smtp.mail.com)
  • smtp: Use ANY SMTP server via host=. tls=False by default
  • smtps: Use ANY SMTP server via host=. tls=True by default

To test emails without sending them, use stub=True option. This queues email info into SMTPStub.stubs without sending it. Use stub='log' to print the email contents as well.

Source code in gramex\services\emailer.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def __init__(
    self,
    type: str,
    email: str = None,
    password: str = None,
    host: str = None,
    port: int = None,
    tls: bool = True,
    stub: str = None,
):
    '''
    Parameters:
        type: Email service type
        email: SMTP server login email ID
        password: SMTP server login email password
        host: SMTP server. Not required when a `type` is specified
        port: SMTP server port. Defaults to 25 for non-TLS, 587 for TLS
        tls: True to use TLS, False to use non-TLS
        stub: 'log' prints email contents instead of sending it

    `type` can be:

    - `gmail`: Google Mail (smtp.gmail.com)
    - `yahoo`: Yahoo Mail (smtp.mail.yahoo.com)
    - `live`: Live.com mail (smtp.live.com)
    - `mandrill`: Mandrill (smtp.mandrillapp.com)
    - `office365`: Office 365 (smtp.office365.com)
    - `outlook`: Outlook (smtp-mail.outlook.com)
    - `icloud`: Apple iCloud (smtp.mail.me.com)
    - `mail`.com: Mail.com (smtp.mail.com)
    - `smtp`: Use ANY SMTP server via `host=`. tls=False by default
    - `smtps`: Use ANY SMTP server via `host=`. tls=True by default

    To test emails without sending them, use `stub=True` option. This queues email info into
    `SMTPStub.stubs` without sending it. Use `stub='log'` to print the email contents as well.
    '''
    self.type = type
    self.email = email
    self.password = password
    self.stub = stub
    if type not in self.clients:
        raise ValueError(f'Unknown email type: {type}')
    self.client = self.clients[type]
    for key, val in (('host', host), ('port', port), ('tls', tls)):
        if val is not None:
            self.client[key] = val
    if 'host' not in self.client:
        raise ValueError('Missing SMTP host')

mail(kwargs)

Sends an email.

Examples:

>>> mailer = SMTPMailer(type='gmail', email='gramex.guide@gmail.com', password='...')
>>> mailer.mail(
... to='person@example.com',
... subject='Subject',
... html='<strong>Bold text</strong>. <img src="cid:logo">'
... body='This plain text is shown if the client cannot render HTML',
... attachments=['1.pdf', '2.txt'],
... images={'logo': '/path/to/logo.png'})
>>> message(to='b@example.org', subject=sub, body=text, html=html)
>>> message(to='b@example.org', subject=sub, body=text, attachments=['file.pdf'])
>>> message(to='b@example.org', subject=sub, body=text, attachments=[
        {'filename': 'test.txt', 'body': 'File contents'}
    ])
>>> message(to='b@example.org', subject=sub, html='<img src="cid:logo">',
            images={'logo': 'd:/images/logo.png'})

Parameters may be any email parameter in RFC 2822. Parameters are case insensitive. Most commonly used are:

  • to: The recipient email address
  • cc: The carbon copy recipient email address
  • bcc: The blind carbon copy recipient email address
  • reply_to: The reply-to email address
  • on_behalf_of: The sender email address
  • subject: The email subject
  • body: text content of the email
  • html: HTML content of the email. If both html and body are specified, the email contains both parts. Email clients may decide to show one or the other.
  • attachments: an list of file names or dict with:
    • body: a byte array of the content
    • content_type: MIME type or filename indicating the file name
  • images: dict of {key: path}.
    • key may be anything. The HTML should include the image via <img src="cid:key">
    • path is the absolute path to the image

In addition, any keyword arguments passed are treated as message headers.

To, Cc and Bcc may be:

  • a string with comma-separated emails, e.g. 'a@x.com, b@x.com'
  • a list of strings with emails, e.g. ['a@x.com', 'b@x.com']
  • a list of strings with comma-separated emails, e.g. ['a@x.com', 'b@x.com, c@x.com']
Source code in gramex\services\emailer.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def mail(self, **kwargs):
    '''Sends an email.

    Examples:
        >>> mailer = SMTPMailer(type='gmail', email='gramex.guide@gmail.com', password='...')
        >>> mailer.mail(
        ... to='person@example.com',
        ... subject='Subject',
        ... html='<strong>Bold text</strong>. <img src="cid:logo">'
        ... body='This plain text is shown if the client cannot render HTML',
        ... attachments=['1.pdf', '2.txt'],
        ... images={'logo': '/path/to/logo.png'})
        >>> message(to='b@example.org', subject=sub, body=text, html=html)
        >>> message(to='b@example.org', subject=sub, body=text, attachments=['file.pdf'])
        >>> message(to='b@example.org', subject=sub, body=text, attachments=[
                {'filename': 'test.txt', 'body': 'File contents'}
            ])
        >>> message(to='b@example.org', subject=sub, html='<img src="cid:logo">',
                    images={'logo': 'd:/images/logo.png'})

    Parameters may be any email parameter in [RFC 2822](https://www.rfc-wiki.org/wiki/RFC2822).
    Parameters are case insensitive. Most commonly used are:

    - `to`: The recipient email address
    - `cc`: The carbon copy recipient email address
    - `bcc`: The blind carbon copy recipient email address
    - `reply_to`: The reply-to email address
    - `on_behalf_of`: The sender email address
    - `subject`: The email subject
    - `body`: text content of the email
    - `html`: HTML content of the email. If both `html` and `body`
        are specified, the email contains both parts. Email clients may decide to
        show one or the other.
    - `attachments`: an list of file names or dict with:
        - `body`: a byte array of the content
        - `content_type`: MIME type or `filename` indicating the file name
    - `images`: dict of `{key: path}`.
        - `key` may be anything. The HTML should include the image via `<img src="cid:key">`
        - `path` is the absolute path to the image

    In addition, any keyword arguments passed are treated as message headers.

    `To`, `Cc` and `Bcc` may be:

    - a string with comma-separated emails, e.g. `'a@x.com, b@x.com'`
    - a list of strings with emails, e.g. `['a@x.com', 'b@x.com']`
    - a list of strings with comma-separated emails, e.g. `['a@x.com', 'b@x.com, c@x.com']`
    '''
    sender = kwargs.get('sender', self.email)
    # SES allows restricting the From: address. https://amzn.to/2Kqwh2y
    # SES uses an IAM ID for login (self.email), but restricts sender -- so from= is required
    # Mailgun suggests From: be the same as Sender: http://bit.ly/2tGS5wt
    # If mail(from=) is specified, use that as the sender. Else use email: from the service
    if kwargs.get('from', None):
        sender = kwargs['from']
    else:
        kwargs['from'] = sender
    # Identify recipients from to/cc/bcc fields.
    # Note: We MUST explicitly add EVERY recipient (to/cc/bcc) in sendmail(recipients=)
    to = recipients(**kwargs)
    msg = message(**kwargs)
    tls = self.client.get('tls', True)
    # Test cases specify stub: true. This uses a stub that logs emails
    if self.stub:
        server = SMTPStub(
            self.client['host'], self.client.get('port', self.ports[tls]), self.stub
        )
    else:
        server = smtplib.SMTP(self.client['host'], self.client.get('port', self.ports[tls]))
    if tls:
        server.starttls()
    if self.email is not None and self.password is not None:
        server.login(self.email, self.password)
    server.sendmail(sender, to, msg.as_string())
    server.quit()
    app_log.info(f'Email sent via {self.client["host"]} ({self.email}) to {", ".join(to)}')

mail_log(kwargs)

Same as mail() but logs the exception. Useful with running in a thread

Source code in gramex\services\emailer.py
169
170
171
172
173
174
175
def mail_log(self, **kwargs):
    '''Same as mail() but logs the exception. Useful with running in a thread'''
    try:
        self.mail(**kwargs)
    except Exception:
        app_log.exception(f'SMTP failed: {kwargs}')
        raise