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.mime.text import MIMEText as _MIMEText
23 from email.header import Header as _Header
24 from email.utils import formataddr as _formataddr
25 from email.utils import parseaddr as _parseaddr
26 import smtplib as _smtplib
27 import subprocess as _subprocess
29 from . import LOG as _LOG
30 from . import config as _config
31 from . import error as _error
34 def guess_encoding(string, encodings=('US-ASCII', 'UTF-8')):
35 """Find an encoding capable of encoding `string`.
37 >>> guess_encoding('alpha', encodings=('US-ASCII', 'UTF-8'))
39 >>> guess_encoding('α', encodings=('US-ASCII', 'UTF-8'))
41 >>> guess_encoding('α', encodings=('US-ASCII', 'ISO-8859-1'))
42 Traceback (most recent call last):
44 rss2email.error.NoValidEncodingError: no valid encoding for α in ('US-ASCII', 'ISO-8859-1')
46 for encoding in encodings:
48 string.encode(encoding)
49 except (UnicodeError, LookupError):
53 raise _error.NoValidEncodingError(string=string, encodings=encodings)
55 def get_message(sender, recipient, subject, body, content_type,
56 extra_headers=None, config=None, section='DEFAULT'):
57 """Generate a `Message` instance.
59 All arguments should be Unicode strings (plain ASCII works as well).
61 Only the real name part of sender and recipient addresses may contain
64 The email will be properly MIME encoded.
66 The charset of the email will be the first one out of the list
67 that can represent all the characters occurring in the email.
69 >>> message = get_message(
70 ... sender='John <jdoe@a.com>', recipient='Ζεύς <z@olympus.org>',
71 ... subject='Testing',
72 ... body='Hello, world!\\n',
73 ... content_type='plain',
74 ... extra_headers={'Approved': 'joe@bob.org'})
75 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
77 Content-Type: text/plain; charset="us-ascii"
78 Content-Transfer-Encoding: 7bit
79 From: John <jdoe@a.com>
80 To: =?utf-8?b?zpbOtc+Nz4I=?= <z@olympus.org>
88 config = _config.CONFIG
90 x.strip() for x in config.get(section, 'encodings').split(',')]
92 # Split real name (which is optional) and email address parts
93 sender_name,sender_addr = _parseaddr(sender)
94 recipient_name,recipient_addr = _parseaddr(recipient)
96 sender_encoding = guess_encoding(sender_name, encodings)
97 recipient_encoding = guess_encoding(recipient_name, encodings)
98 subject_encoding = guess_encoding(subject, encodings)
99 body_encoding = guess_encoding(body, encodings)
101 # We must always pass Unicode strings to Header, otherwise it will
102 # use RFC 2047 encoding even on plain ASCII strings.
103 sender_name = str(_Header(sender_name, sender_encoding).encode())
104 recipient_name = str(_Header(recipient_name, recipient_encoding).encode())
106 # Make sure email addresses do not contain non-ASCII characters
107 sender_addr.encode('ascii')
108 recipient_addr.encode('ascii')
110 # Create the message ('plain' stands for Content-Type: text/plain)
111 message = _MIMEText(body, content_type, body_encoding)
112 message['From'] = _formataddr((sender_name, sender_addr))
113 message['To'] = _formataddr((recipient_name, recipient_addr))
114 message['Subject'] = _Header(subject, subject_encoding)
115 if config.getboolean(section, 'use_8bit'):
116 message['Content-Transfer-Encoding'] = '8bit'
117 message.set_payload(body)
119 for key,value in extra_headers.items():
120 encoding = guess_encoding(value, encodings)
121 message[key] = _Header(value, encoding)
124 def smtp_send(sender, recipient, message, config=None, section='DEFAULT'):
126 config = _config.CONFIG
127 server = config.get(section, 'smtp-server')
128 _LOG.debug('sending message to {} via {}'.format(recipient, server))
129 ssl = config.getboolean(section, 'smtp-ssl')
131 smtp = _smtplib.SMTP_SSL()
133 smtp = _smtplib.SMTP()
136 smtp.connect(SMTP_SERVER)
137 except KeyboardInterrupt:
139 except Exception as e:
140 raise _error.SMTPConnectionError(server=server) from e
141 if config.getboolean(section, 'smtp-auth'):
142 username = config.get(section, 'smtp-username')
143 password = config.get(section, 'smtp-password')
147 smtp.login(username, password)
148 except KeyboardInterrupt:
150 except Exception as e:
151 raise _error.SMTPAuthenticationError(
152 server=server, username=username)
153 smtp.send_message(message, sender, [recipient])
156 def sendmail_send(sender, recipient, message, config=None, section='DEFAULT'):
158 config = _config.CONFIG
160 'sending message to {} via /usr/sbin/sendmail'.format(recipient))
162 p = _subprocess.Popen(
163 ['/usr/sbin/sendmail', recipient],
164 stdin=_subprocess.PIPE, stdout=_subprocess.PIPE,
165 stderr=_subprocess.PIPE)
166 stdout,stderr = p.communicate(message.as_string()
167 .encode(str(message.get_charset())))
170 raise _error.SendmailError(
171 status=status, stdout=stdout, stderr=stderr)
172 except Exception as e:
173 raise _error.SendmailError() from e
175 def send(sender, recipient, message, config=None, section='DEFAULT'):
176 if config.getboolean(section, 'use-smtp'):
177 smtp_send(sender, recipient, message)
179 sendmail_send(sender, recipient, message)