1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
|
#!/usr/bin/env python
"""Send a review to the launchpad-reviews mailing list.
The review should live in a text file which you name on the command line. The
text file must have the full path of the review branch in the first 10 lines.
This script will sniff the branch and CC the person whose branch this is.
"""
from __future__ import with_statement
import os
import pwd
import sys
import time
import smtplib
import optparse
import urlparse
from email.mime.text import MIMEText
from email.utils import formataddr
LAUNCHPAD_REVIEWS = 'Launchpad Reviews <launchpad-reviews@lists.canonical.com>'
SLASH = '/'
COMMASPACE = ', '
COLON = ':'
# Hack the urlparse module global because it stupidly hardcodes the scheme
# identifiers, doesn't know about bzr+ssh, and doesn't provide an API for
# amending the schemes.
urlparse.uses_netloc.append('bzr+ssh')
def server_callback(option, opt, value, parser):
"""Return (host, port) parsed from string."""
if COLON in value:
host, port = value.split(COLON, 1)
if host == '':
host = 'localhost'
if port == '':
port = 0
port = int(port)
else:
# No port given. 0 tells smtplib to use the system default.
host = value
port = 0
setattr(parser.values, option.dest, (host, port))
def parseargs():
parser = optparse.OptionParser(usage="""\
%prog [options] filename
'filename' is the name of a text file containing your review. You must
include the full branch url on a line by itself within the first 10 lines of
the file. Leading whitespace on the branch url line is ignored.
This script parses the branch author's recipient email address from the branch
url, requiring that the user portion of the path on devpad correspond directly
to a deliverable email address @canonical.com. E.g.:
bzr+ssh://devpad.canonical.com/code/barry/launchpad/review-script
For example, say you were reviewing the above branch, and you had a text file
containing your marked up review. Put that branch url within the first 10
lines, and let's say the file was 'script.txt'. You'd send this review
with the following command line:
% utilities/review script.txt""")
parser.add_option('-s', '--smtp',
type='string', default=None,
action='callback', callback=server_callback,
help="""\
hostname:port for the SMTP server to use. Both the hostname and the port part
is optional, with localhost being the default hostname, and 25 being the
default port. If neither is given localhost:25 is used.""")
parser.add_option('-f', '--from',
type='string', default=None, dest='sender',
help="""\
The From: header value used. The value may be any string parseable by
email.utils.parseaddr(). The address portion of the string will be used as
the envelope sender (i.e. the MAIL FROM). If not given, the user's short name
as calculated from the /etc/password file is used, with @canonical.com
appended, along with the user's full name as calculated from the /etc/password
file.""")
parser.add_option('-t', '--to',
type='string', default=None, dest='to_recipient',
help="""\
The To: header value used. The value may be any string parseable by
email.utils.parseaddr(). The address portion of the string will be used as
an envelope recipient (i.e. the RCPT TO). If not given, the recipient's
address is calculated from the branch url.""")
parser.add_option('-c', '--cc',
type='string', default=[], action='append',
dest='cc_recipients',
help="""\
The Cc: header value used. The value may be any string parseable by
email.utils.parseaddr(). The address portion of the string will be used as
an envelope recipient (i.e. the RCPT TO). Note that
launchpad-reviews@canonical.com is always CC'd.""")
parser.add_option('-d', '--debug-mode',
default=0, action='count',
help="""\
Debug mode. With one -d the message is printed to stdout and no email is
sent. With two -d flags, an email is sent only to you (via the /etc/password
calculated sender or the -f flag) and not to the mailing list.""")
opts, args = parser.parse_args()
if len(args) < 1:
parser.error('Missing review text file name')
if len(args) > 1:
parser.error('Too many arguments')
return parser, opts, args
# I always put the branch name in one of the first few lines
def get_branch_and_user(filename):
"""Parse the branch and user names from a branch url in the file.
Return a 2-tuple of (branch, username). If no matching branch url can be
found within the first 10 lines of the file, (None, None) is returned.
"""
with open(filename) as fp:
for i, line in enumerate(fp):
branch = line.strip()
parts = urlparse.urlsplit(branch)
if not parts.scheme or not parts.netloc.startswith('devpad'):
if i < 10:
continue
break
path = parts.path.split(SLASH)
if path[1] <> 'code' or path[2] == 'launchpad':
continue
return SLASH.join(path[2:]), path[2]
return None, None
def main():
parser, opts, args = parseargs()
branch, user = get_branch_and_user(args[0])
if branch is None:
print >> sys.stderr, 'No branch url was found in first 10 lines'
return 1
if opts.to_recipient is None:
to_recipient = user + '@canonical.com'
else:
to_recipient = opts.to_recipient
cc_recipients = [LAUNCHPAD_REVIEWS]
cc_recipients.extend(opts.cc_recipients)
envto = [to_recipient]
envto.extend(cc_recipients)
if opts.sender is None:
pw_entry = pwd.getpwuid(os.getuid())
real_name = pw_entry.pw_gecos.split(',')[0]
# This is probably not generically correct, but it works for me :)
canonical_who = pw_entry.pw_name + '@canonical.com'
sender = formataddr((real_name, canonical_who))
else:
sender = opts.sender
envfrom = sender
# Craft the email message.
with open(args[0]) as fp:
msg = MIMEText(fp.read())
msg['From'] = sender
msg['Subject'] = 'REVIEW: ' + branch
msg['To'] = to_recipient
msg['Cc'] = COMMASPACE.join(cc_recipients)
# In debug mode 1, we print everything to stdout.
if opts.debug_mode == 1:
print 'MAIL FROM:', envfrom
for recipient in envto:
print 'RCPT TO:', recipient
print msg.as_string()
return 0
# In debug mode 2, we mail the message to ourselves.
elif opts.debug_mode > 1:
envto = [sender]
else:
# We already have the SMTP recipients set up as expected, so there's
# nothing more to do.
pass
# Send the message.
s = smtplib.SMTP()
if opts.smtp is None:
s.connect()
else:
host, port = opts.smtp
s.connect(host, port)
s.sendmail(envfrom, envto, str(msg))
s.quit()
return 0
if __name__ == '__main__':
sys.exit(main())
|