36
37
self.config = config
38
39
def compose(self, prompt, to, subject, attachment, mime_subtype,
40
extension, basename=None):
40
41
"""Compose (and possibly send) an email message
42
43
Must be implemented by subclasses.
51
52
"plain", "x-patch", etc.
52
53
:param extension: The file extension associated with the attachment
53
54
type, e.g. ".patch"
55
:param basename: The name to use for the attachment, e.g.
55
58
raise NotImplementedError
57
def compose_merge_request(self, to, subject, directive):
60
def compose_merge_request(self, to, subject, directive, basename=None):
58
61
"""Compose (and possibly send) a merge request
60
63
:param to: The address to send the request to
61
64
:param subject: The subject line to use for the request
62
65
:param directive: A merge directive representing the merge request, as
67
:param basename: The name to use for the attachment, e.g.
65
70
prompt = self._get_merge_prompt("Please describe these changes:", to,
66
71
subject, directive)
67
72
self.compose(prompt, to, subject, directive,
73
'x-patch', '.patch', basename)
70
75
def _get_merge_prompt(self, prompt, to, subject, attachment):
71
76
"""Generate a prompt string. Overridden by Editor.
90
95
attachment.decode('utf-8', 'replace')))
92
97
def compose(self, prompt, to, subject, attachment, mime_subtype,
98
extension, basename=None):
94
99
"""See MailClient.compose"""
96
101
raise errors.NoMailAddressSpecified()
118
123
return self._client_commands
120
125
def compose(self, prompt, to, subject, attachment, mime_subtype,
126
extension, basename=None):
122
127
"""See MailClient.compose.
124
129
Writes the attachment to a temporary file, invokes _compose.
126
fd, pathname = tempfile.mkstemp(extension, 'bzr-mail-')
132
basename = 'attachment'
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
134
attach_path = osutils.pathjoin(pathname, basename + extension)
135
outfile = open(attach_path, 'wb')
128
os.write(fd, attachment)
137
outfile.write(attachment)
131
self._compose(prompt, to, subject, pathname, mime_subtype, extension)
140
self._compose(prompt, to, subject, attach_path, mime_subtype,
133
143
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
144
154
the attachment type.
146
156
for name in self._get_client_commands():
157
cmdline = [self._encode_path(name, 'executable')]
148
158
cmdline.extend(self._get_compose_commandline(to, subject,
168
178
raise NotImplementedError
180
def _encode_safe(self, u):
181
"""Encode possible unicode string argument to 8-bit string
182
in user_encoding. Unencodable characters will be replaced
185
:param u: possible unicode string.
186
:return: encoded string if u is unicode, u itself otherwise.
188
if isinstance(u, unicode):
189
return u.encode(bzrlib.user_encoding, 'replace')
192
def _encode_path(self, path, kind):
193
"""Encode unicode path in user encoding.
195
:param path: possible unicode path.
196
:param kind: path kind ('executable' or 'attachment').
197
:return: encoded path if path is unicode,
198
path itself otherwise.
199
:raise: UnableEncodePath.
201
if isinstance(path, unicode):
203
return path.encode(bzrlib.user_encoding)
204
except UnicodeEncodeError:
205
raise errors.UnableEncodePath(path, kind)
171
209
class Evolution(ExternalMailClient):
172
210
"""Evolution mail client."""
181
219
if attach_path is not None:
182
220
message_options['attach'] = attach_path
183
221
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
184
message_options.iteritems()]
185
return ['mailto:%s?%s' % (to or '', '&'.join(options_list))]
222
sorted(message_options.iteritems())]
223
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
224
'&'.join(options_list))]
188
227
class Mutt(ExternalMailClient):
194
233
"""See ExternalMailClient._get_compose_commandline"""
195
234
message_options = []
196
235
if subject is not None:
197
message_options.extend(['-s', subject ])
236
message_options.extend(['-s', self._encode_safe(subject)])
198
237
if attach_path is not None:
199
message_options.extend(['-a', attach_path])
238
message_options.extend(['-a',
239
self._encode_path(attach_path, 'attachment')])
200
240
if to is not None:
201
message_options.append(to)
241
message_options.append(self._encode_safe(to))
202
242
return message_options
219
259
"""See ExternalMailClient._get_compose_commandline"""
220
260
message_options = {}
221
261
if to is not None:
222
message_options['to'] = to
262
message_options['to'] = self._encode_safe(to)
223
263
if subject is not None:
224
message_options['subject'] = subject
264
message_options['subject'] = self._encode_safe(subject)
225
265
if attach_path is not None:
226
266
message_options['attachment'] = urlutils.local_path_to_url(
239
279
"""See ExternalMailClient._get_compose_commandline"""
240
280
message_options = []
241
281
if subject is not None:
242
message_options.extend( ['-s', subject ] )
282
message_options.extend(['-s', self._encode_safe(subject)])
243
283
if attach_path is not None:
244
message_options.extend( ['--attach', attach_path] )
284
message_options.extend(['--attach',
285
self._encode_path(attach_path, 'attachment')])
245
286
if to is not None:
246
message_options.extend( [ to ] )
287
message_options.extend([self._encode_safe(to)])
248
288
return message_options
257
297
"""See ExternalMailClient._get_compose_commandline"""
259
299
raise errors.NoMailAddressSpecified()
300
commandline = [self._encode_safe(to)]
261
301
if subject is not None:
262
commandline.extend(['--subject', subject])
302
commandline.extend(['--subject', self._encode_safe(subject)])
263
303
if attach_path is not None:
264
commandline.extend(['--attach', attach_path])
304
commandline.extend(['--attach',
305
self._encode_path(attach_path, 'attachment')])
265
306
return commandline
295
336
return XDGEmail(self.config)
297
338
def compose(self, prompt, to, subject, attachment, mime_subtype,
339
extension, basename=None):
299
340
"""See MailClient.compose"""
301
342
return self._mail_client().compose(prompt, to, subject,
302
343
attachment, mimie_subtype,
304
345
except errors.MailClientNotFound:
305
346
return Editor(self.config).compose(prompt, to, subject,
306
347
attachment, mimie_subtype, extension)
308
def compose_merge_request(self, to, subject, directive):
349
def compose_merge_request(self, to, subject, directive, basename=None):
309
350
"""See MailClient.compose_merge_request"""
311
352
return self._mail_client().compose_merge_request(to, subject,
353
directive, basename=basename)
313
354
except errors.MailClientNotFound:
314
355
return Editor(self.config).compose_merge_request(to, subject,
356
directive, basename=basename)