1 # -*- encoding: utf-8 -*-
3 # Copyright (C) 2012-2013 W. Trevor King <wking@tremily.us>
5 # This file is part of rss2email.
7 # rss2email is free software: you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free Software
9 # Foundation, either version 2 of the License, or (at your option) version 3 of
12 # rss2email is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along with
17 # rss2email. If not, see <http://www.gnu.org/licenses/>.
19 """Email message generation and dispatching
22 from email.charset import Charset as _Charset
23 import email.encoders as _email_encoders
24 from email.generator import BytesGenerator as _BytesGenerator
25 from email.header import Header as _Header
26 from email.mime.text import MIMEText as _MIMEText
27 from email.utils import formataddr as _formataddr
28 from email.utils import parseaddr as _parseaddr
30 import smtplib as _smtplib
31 import subprocess as _subprocess
33 from . import LOG as _LOG
34 from . import config as _config
35 from . import error as _error
38 def guess_encoding(string, encodings=('US-ASCII', 'UTF-8')):
39 """Find an encoding capable of encoding `string`.
41 >>> guess_encoding('alpha', encodings=('US-ASCII', 'UTF-8'))
43 >>> guess_encoding('α', encodings=('US-ASCII', 'UTF-8'))
45 >>> guess_encoding('α', encodings=('US-ASCII', 'ISO-8859-1'))
46 Traceback (most recent call last):
48 rss2email.error.NoValidEncodingError: no valid encoding for α in ('US-ASCII', 'ISO-8859-1')
50 for encoding in encodings:
52 string.encode(encoding)
53 except (UnicodeError, LookupError):
57 raise _error.NoValidEncodingError(string=string, encodings=encodings)
59 def get_message(sender, recipient, subject, body, content_type,
60 extra_headers=None, config=None, section='DEFAULT'):
61 """Generate a `Message` instance.
63 All arguments should be Unicode strings (plain ASCII works as well).
65 Only the real name part of sender and recipient addresses may contain
68 The email will be properly MIME encoded.
70 The charset of the email will be the first one out of the list
71 that can represent all the characters occurring in the email.
73 >>> message = get_message(
74 ... sender='John <jdoe@a.com>', recipient='Ζεύς <z@olympus.org>',
75 ... subject='Testing',
76 ... body='Hello, world!\\n',
77 ... content_type='plain',
78 ... extra_headers={'Approved': 'joe@bob.org'})
79 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
81 Content-Type: text/plain; charset="us-ascii"
82 Content-Transfer-Encoding: 7bit
83 From: John <jdoe@a.com>
84 To: =?utf-8?b?zpbOtc+Nz4I=?= <z@olympus.org>
92 config = _config.CONFIG
93 if section not in config.sections():
96 x.strip() for x in config.get(section, 'encodings').split(',')]
98 # Split real name (which is optional) and email address parts
99 sender_name,sender_addr = _parseaddr(sender)
100 recipient_name,recipient_addr = _parseaddr(recipient)
102 sender_encoding = guess_encoding(sender_name, encodings)
103 recipient_encoding = guess_encoding(recipient_name, encodings)
104 subject_encoding = guess_encoding(subject, encodings)
105 body_encoding = guess_encoding(body, encodings)
107 # We must always pass Unicode strings to Header, otherwise it will
108 # use RFC 2047 encoding even on plain ASCII strings.
109 sender_name = str(_Header(sender_name, sender_encoding).encode())
110 recipient_name = str(_Header(recipient_name, recipient_encoding).encode())
112 # Make sure email addresses do not contain non-ASCII characters
113 sender_addr.encode('ascii')
114 recipient_addr.encode('ascii')
116 # Create the message ('plain' stands for Content-Type: text/plain)
117 message = _MIMEText(body, content_type, body_encoding)
118 message['From'] = _formataddr((sender_name, sender_addr))
119 message['To'] = _formataddr((recipient_name, recipient_addr))
120 message['Subject'] = _Header(subject, subject_encoding)
121 if config.getboolean(section, 'use-8bit'):
122 del message['Content-Transfer-Encoding']
123 charset = _Charset(body_encoding)
124 charset.body_encoding = _email_encoders.encode_7or8bit
125 message.set_payload(body.encode(body_encoding), charset=charset)
127 for key,value in extra_headers.items():
128 encoding = guess_encoding(value, encodings)
129 message[key] = _Header(value, encoding)
132 def smtp_send(sender, recipient, message, config=None, section='DEFAULT'):
134 config = _config.CONFIG
135 server = config.get(section, 'smtp-server')
136 _LOG.debug('sending message to {} via {}'.format(recipient, server))
137 ssl = config.getboolean(section, 'smtp-ssl')
139 smtp = _smtplib.SMTP_SSL()
141 smtp = _smtplib.SMTP()
144 smtp.connect(SMTP_SERVER)
145 except KeyboardInterrupt:
147 except Exception as e:
148 raise _error.SMTPConnectionError(server=server) from e
149 if config.getboolean(section, 'smtp-auth'):
150 username = config.get(section, 'smtp-username')
151 password = config.get(section, 'smtp-password')
155 smtp.login(username, password)
156 except KeyboardInterrupt:
158 except Exception as e:
159 raise _error.SMTPAuthenticationError(
160 server=server, username=username)
161 smtp.send_message(message, sender, [recipient])
164 def _flatten(message):
165 r"""Flatten an email.message.Message to bytes
167 >>> import rss2email.config
168 >>> config = rss2email.config.Config()
169 >>> config.read_dict(rss2email.config.CONFIG)
171 Here's a 7-bit, base64 version:
173 >>> message = get_message(
174 ... sender='John <jdoe@a.com>', recipient='Ζεύς <z@olympus.org>',
175 ... subject='Homage',
176 ... body="You're great, Ζεύς!\n",
177 ... content_type='plain',
179 >>> for line in _flatten(message).split(b'\n'):
180 ... print(line) # doctest: +REPORT_UDIFF
182 b'Content-Type: text/plain; charset="utf-8"'
183 b'Content-Transfer-Encoding: base64'
184 b'From: John <jdoe@a.com>'
185 b'To: =?utf-8?b?zpbOtc+Nz4I=?= <z@olympus.org>'
188 b'WW91J3JlIGdyZWF0LCDOls61z43PgiEK'
191 Here's an 8-bit version:
193 >>> config.set('DEFAULT', 'use-8bit', str(True))
194 >>> message = get_message(
195 ... sender='John <jdoe@a.com>', recipient='Ζεύς <z@olympus.org>',
196 ... subject='Homage',
197 ... body="You're great, Ζεύς!\n",
198 ... content_type='plain',
200 >>> for line in _flatten(message).split(b'\n'):
201 ... print(line) # doctest: +REPORT_UDIFF
203 b'Content-Type: text/plain; charset="utf-8"'
204 b'From: John <jdoe@a.com>'
205 b'To: =?utf-8?b?zpbOtc+Nz4I=?= <z@olympus.org>'
207 b'Content-Transfer-Encoding: 8bit'
209 b"You're great, \xce\x96\xce\xb5\xcf\x8d\xcf\x82!"
212 Here's an 8-bit version in UTF-16:
214 >>> config.set('DEFAULT', 'encodings', 'US-ASCII, UTF-16-LE')
215 >>> message = get_message(
216 ... sender='John <jdoe@a.com>', recipient='Ζεύς <z@olympus.org>',
217 ... subject='Homage',
218 ... body="You're great, Ζεύς!\n",
219 ... content_type='plain',
221 >>> for line in _flatten(message).split(b'\n'):
222 ... print(line) # doctest: +REPORT_UDIFF
224 b'Content-Type: text/plain; charset="utf-16-le"'
225 b'From: John <jdoe@a.com>'
226 b'To: =?utf-8?b?zpbOtc+Nz4I=?= <z@olympus.org>'
228 b'Content-Transfer-Encoding: 8bit'
230 b"\x00Y\x00o\x00u\x00'\x00r\x00e\x00 \x00g\x00r\x00e\x00a\x00t\x00,\x00 \x00\x96\x03\xb5\x03\xcd\x03\xc2\x03!\x00\n\x00"
232 bytesio = _io.BytesIO()
233 generator = _BytesGenerator(bytesio) # use policies for Python >=3.3
234 generator.flatten(message)
235 return bytesio.getvalue()
237 def sendmail_send(sender, recipient, message, config=None, section='DEFAULT'):
239 config = _config.CONFIG
240 message_bytes = _flatten(message)
241 sendmail = config.get(section, 'sendmail')
243 'sending message to {} via {}'.format(recipient, sendmail))
245 p = _subprocess.Popen(
246 [sendmail, '-f', sender, recipient],
247 stdin=_subprocess.PIPE, stdout=_subprocess.PIPE,
248 stderr=_subprocess.PIPE)
249 stdout,stderr = p.communicate(message_bytes)
252 raise _error.SendmailError(
253 status=status, stdout=stdout, stderr=stderr)
254 except Exception as e:
255 raise _error.SendmailError() from e
257 def send(sender, recipient, message, config=None, section='DEFAULT'):
258 if config.getboolean(section, 'use-smtp'):
259 smtp_send(sender, recipient, message)
261 sendmail_send(sender, recipient, message)