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