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