135
|
1 # mail.py - mail sending bits for mercurial
|
|
2 #
|
|
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
|
|
4 #
|
|
5 # This software may be used and distributed according to the terms of the
|
|
6 # GNU General Public License version 2, incorporated herein by reference.
|
|
7
|
|
8 from i18n import _
|
|
9 import util, encoding
|
|
10 import os, smtplib, socket, quopri
|
|
11 import email.Header, email.MIMEText, email.Utils
|
|
12
|
|
13 def _smtp(ui):
|
|
14 '''build an smtp connection and return a function to send mail'''
|
|
15 local_hostname = ui.config('smtp', 'local_hostname')
|
|
16 s = smtplib.SMTP(local_hostname=local_hostname)
|
|
17 mailhost = ui.config('smtp', 'host')
|
|
18 if not mailhost:
|
|
19 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
|
|
20 mailport = int(ui.config('smtp', 'port', 25))
|
|
21 ui.note(_('sending mail: smtp host %s, port %s\n') %
|
|
22 (mailhost, mailport))
|
|
23 s.connect(host=mailhost, port=mailport)
|
|
24 if ui.configbool('smtp', 'tls'):
|
|
25 if not hasattr(socket, 'ssl'):
|
|
26 raise util.Abort(_("can't use TLS: Python SSL support "
|
|
27 "not installed"))
|
|
28 ui.note(_('(using tls)\n'))
|
|
29 s.ehlo()
|
|
30 s.starttls()
|
|
31 s.ehlo()
|
|
32 username = ui.config('smtp', 'username')
|
|
33 password = ui.config('smtp', 'password')
|
|
34 if username and not password:
|
|
35 password = ui.getpass()
|
|
36 if username and password:
|
|
37 ui.note(_('(authenticating to mail server as %s)\n') %
|
|
38 (username))
|
|
39 s.login(username, password)
|
|
40
|
|
41 def send(sender, recipients, msg):
|
|
42 try:
|
|
43 return s.sendmail(sender, recipients, msg)
|
|
44 except smtplib.SMTPRecipientsRefused, inst:
|
|
45 recipients = [r[1] for r in inst.recipients.values()]
|
|
46 raise util.Abort('\n' + '\n'.join(recipients))
|
|
47 except smtplib.SMTPException, inst:
|
|
48 raise util.Abort(inst)
|
|
49
|
|
50 return send
|
|
51
|
|
52 def _sendmail(ui, sender, recipients, msg):
|
|
53 '''send mail using sendmail.'''
|
|
54 program = ui.config('email', 'method')
|
|
55 cmdline = '%s -f %s %s' % (program, util.email(sender),
|
|
56 ' '.join(map(util.email, recipients)))
|
|
57 ui.note(_('sending mail: %s\n') % cmdline)
|
|
58 fp = util.popen(cmdline, 'w')
|
|
59 fp.write(msg)
|
|
60 ret = fp.close()
|
|
61 if ret:
|
|
62 raise util.Abort('%s %s' % (
|
|
63 os.path.basename(program.split(None, 1)[0]),
|
|
64 util.explain_exit(ret)[0]))
|
|
65
|
|
66 def connect(ui):
|
|
67 '''make a mail connection. return a function to send mail.
|
|
68 call as sendmail(sender, list-of-recipients, msg).'''
|
|
69 if ui.config('email', 'method', 'smtp') == 'smtp':
|
|
70 return _smtp(ui)
|
|
71 return lambda s, r, m: _sendmail(ui, s, r, m)
|
|
72
|
|
73 def sendmail(ui, sender, recipients, msg):
|
|
74 send = connect(ui)
|
|
75 return send(sender, recipients, msg)
|
|
76
|
|
77 def validateconfig(ui):
|
|
78 '''determine if we have enough config data to try sending email.'''
|
|
79 method = ui.config('email', 'method', 'smtp')
|
|
80 if method == 'smtp':
|
|
81 if not ui.config('smtp', 'host'):
|
|
82 raise util.Abort(_('smtp specified as email transport, '
|
|
83 'but no smtp host configured'))
|
|
84 else:
|
|
85 if not util.find_exe(method):
|
|
86 raise util.Abort(_('%r specified as email transport, '
|
|
87 'but not in PATH') % method)
|
|
88
|
|
89 def mimetextpatch(s, subtype='plain', display=False):
|
|
90 '''If patch in utf-8 transfer-encode it.'''
|
|
91
|
|
92 enc = None
|
|
93 for line in s.splitlines():
|
|
94 if len(line) > 950:
|
|
95 s = quopri.encodestring(s)
|
|
96 enc = "quoted-printable"
|
|
97 break
|
|
98
|
|
99 cs = 'us-ascii'
|
|
100 if not display:
|
|
101 try:
|
|
102 s.decode('us-ascii')
|
|
103 except UnicodeDecodeError:
|
|
104 try:
|
|
105 s.decode('utf-8')
|
|
106 cs = 'utf-8'
|
|
107 except UnicodeDecodeError:
|
|
108 # We'll go with us-ascii as a fallback.
|
|
109 pass
|
|
110
|
|
111 msg = email.MIMEText.MIMEText(s, subtype, cs)
|
|
112 if enc:
|
|
113 del msg['Content-Transfer-Encoding']
|
|
114 msg['Content-Transfer-Encoding'] = enc
|
|
115 return msg
|
|
116
|
|
117 def _charsets(ui):
|
|
118 '''Obtains charsets to send mail parts not containing patches.'''
|
|
119 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
|
|
120 fallbacks = [encoding.fallbackencoding.lower(),
|
|
121 encoding.encoding.lower(), 'utf-8']
|
|
122 for cs in fallbacks: # find unique charsets while keeping order
|
|
123 if cs not in charsets:
|
|
124 charsets.append(cs)
|
|
125 return [cs for cs in charsets if not cs.endswith('ascii')]
|
|
126
|
|
127 def _encode(ui, s, charsets):
|
|
128 '''Returns (converted) string, charset tuple.
|
|
129 Finds out best charset by cycling through sendcharsets in descending
|
|
130 order. Tries both encoding and fallbackencoding for input. Only as
|
|
131 last resort send as is in fake ascii.
|
|
132 Caveat: Do not use for mail parts containing patches!'''
|
|
133 try:
|
|
134 s.decode('ascii')
|
|
135 except UnicodeDecodeError:
|
|
136 sendcharsets = charsets or _charsets(ui)
|
|
137 for ics in (encoding.encoding, encoding.fallbackencoding):
|
|
138 try:
|
|
139 u = s.decode(ics)
|
|
140 except UnicodeDecodeError:
|
|
141 continue
|
|
142 for ocs in sendcharsets:
|
|
143 try:
|
|
144 return u.encode(ocs), ocs
|
|
145 except UnicodeEncodeError:
|
|
146 pass
|
|
147 except LookupError:
|
|
148 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
|
|
149 # if ascii, or all conversion attempts fail, send (broken) ascii
|
|
150 return s, 'us-ascii'
|
|
151
|
|
152 def headencode(ui, s, charsets=None, display=False):
|
|
153 '''Returns RFC-2047 compliant header from given string.'''
|
|
154 if not display:
|
|
155 # split into words?
|
|
156 s, cs = _encode(ui, s, charsets)
|
|
157 return str(email.Header.Header(s, cs))
|
|
158 return s
|
|
159
|
|
160 def addressencode(ui, address, charsets=None, display=False):
|
|
161 '''Turns address into RFC-2047 compliant header.'''
|
|
162 if display or not address:
|
|
163 return address or ''
|
|
164 name, addr = email.Utils.parseaddr(address)
|
|
165 name = headencode(ui, name, charsets)
|
|
166 try:
|
|
167 acc, dom = addr.split('@')
|
|
168 acc = acc.encode('ascii')
|
|
169 dom = dom.encode('idna')
|
|
170 addr = '%s@%s' % (acc, dom)
|
|
171 except UnicodeDecodeError:
|
|
172 raise util.Abort(_('invalid email address: %s') % addr)
|
|
173 except ValueError:
|
|
174 try:
|
|
175 # too strict?
|
|
176 addr = addr.encode('ascii')
|
|
177 except UnicodeDecodeError:
|
|
178 raise util.Abort(_('invalid local address: %s') % addr)
|
|
179 return email.Utils.formataddr((name, addr))
|
|
180
|
|
181 def mimeencode(ui, s, charsets=None, display=False):
|
|
182 '''creates mime text object, encodes it if needed, and sets
|
|
183 charset and transfer-encoding accordingly.'''
|
|
184 cs = 'us-ascii'
|
|
185 if not display:
|
|
186 s, cs = _encode(ui, s, charsets)
|
|
187 return email.MIMEText.MIMEText(s, 'plain', cs)
|