Skip to content

Commit 41db98e

Browse files
committed
add ability to configure TLS and Authentication for sending PyPI mail
Also updates the configuration template to match the kinds of things you'd see in production. Configuring for starttls and using authentication will allow us to send mail from transient PyPI web nodes without having to ask for the mail admins to whitelist the sending IP address. This is obviously dependent on a change to the configuration (so deploy to production cautiously). It also depends on the mail provider (mail.python.org in this case) supporting TLS and Authentication, so we may need to follow up with them.
1 parent efdfd36 commit 41db98e

6 files changed

+107
-40
lines changed

MailingLogger.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def format(self,record):
3030

3131
class MailingLogger(SMTPHandler):
3232

33-
def __init__(self, mailhost, fromaddr, toaddrs, subject, send_empty_entries,flood_level=None):
34-
SMTPHandler.__init__(self,mailhost,fromaddr,toaddrs,subject)
33+
def __init__(self, mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, send_empty_entries=False, flood_level=None):
34+
SMTPHandler.__init__(self, mailhost, fromaddr, toaddrs, subject, credentials=credentials, secure=secure)
3535
self.subject_formatter = SubjectFormatter(subject)
3636
self.send_empty_entries = send_empty_entries
3737
self.flood_level = flood_level
@@ -42,8 +42,6 @@ def getSubject(self,record):
4242
return self.subject_formatter.format(record)
4343

4444
def emit(self,record):
45-
if not self.send_empty_entries and not record.msg.strip():
46-
return
4745
current_time = now()
4846
current_hour = current_time.hour
4947
if current_hour > self.hour:
@@ -69,6 +67,8 @@ def emit(self,record):
6967
""" % (self.sent,current_time.strftime('%H:%M:%S'),current_hour+1),
7068
args = (),
7169
exc_info = None)
70+
if not self.send_empty_entries and not record.msg.strip():
71+
return
7272
elif self.sent > self.flood_level:
7373
# do nothing, we've sent too many emails already
7474
return
@@ -87,6 +87,10 @@ def emit(self,record):
8787
email['From']=self.fromaddr
8888
email['To']=', '.join(self.toaddrs)
8989
email['X-Mailer']='MailingLogger'
90+
if self.username:
91+
if self.secure is not None:
92+
smtp.starttls(*self.secure)
93+
smtp.login(self.username, self.password)
9094
smtp.sendmail(self.fromaddr, self.toaddrs, email.as_string())
9195
smtp.quit()
9296
except:

config.ini.template

+57-22
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,57 @@
11
[database]
2-
driver = postgresql2
2+
3+
;Postgres Database
4+
;host = hostname
5+
;port = 5432
36
name = packages
47
user = pypi
5-
# host = hostname
6-
# port = 5432
8+
9+
; Redis
10+
redis_url = redis://localhost:6379/0
11+
12+
; Storage Directories
713
files_dir = /MacDev/svn.python.org/pypi-pep345/files
814
docs_dir = /MacDev/svn.python.org/pypi-pep345/docs
9-
package_docs_url = http://pythonhosted.org/
10-
redis_url = redis://localhost:6379/0
15+
16+
; Third-Party
17+
pubsubhubbub = http://pubsubhubbub.appspot.com/
1118

1219
[webui]
13-
mailhost = mail.python.org
14-
adminemail = richard@python.org
15-
replyto = richard@python.org
16-
url = http://localhost:8000/pypi
17-
pydotorg = http://www.python.org/
1820

19-
simple_script = /simple
20-
files_url = http://localhost/pypi_files
21+
; PyPI config
22+
debug_mode = yes
2123
rss_file = /tmp/pypi_rss.xml
2224
packages_rss_file = /tmp/pypi_packages_rss.xml
23-
debug_mode = yes
24-
cheesecake_password = secret
25+
26+
; Email
27+
adminemail = richard@python.org
28+
replyto = richard@python.org
29+
30+
; Secrets
31+
;sshkeys_update = /opt/devpypi/src/sshkeys_update
2532
key_dir = .
26-
simple_sign_script = /serversig
27-
raw_package_prefix = /raw-packages
33+
cheesecake_password = secret
2834
; this is the secret used to sign password reset efforts - keep it secret!
2935
; ''.join(random.choice(string.letters + string.digits) for n in range(64))
30-
reset_secret = secret
36+
;reset_secret = secret
37+
38+
; URI Paths
39+
simple_script = /simple
40+
raw_package_prefix = /raw-packages
41+
simple_sign_script = /serversig
42+
43+
; URLs
44+
url = http://localhost:8000/pypi
45+
files_url = http://localhost/pypi_files
46+
pydotorg = http://www.python.org/
47+
package_docs_url = http://pythonhosted.org/
48+
49+
[smtp]
50+
hostname = localhost:25
51+
starttls = off
52+
auth = off
53+
;login = postmaster@localhost
54+
;password = muchsecret
3155

3256
[passlib]
3357
; The first listed schemed will automatically be the default, see passlib
@@ -43,26 +67,37 @@ schemes = bcrypt, hex_sha1
4367

4468
[logging]
4569
file =
46-
mailhost =
70+
mail_logger = off
4771
fromaddr =
4872
toaddrs =
4973

50-
[mirrors]
51-
folder = mirrors
52-
local-stats = local-stats
53-
global-stats = global-stats
74+
; Not seeing this used in production
75+
;[mirrors]
76+
;folder = mirrors
77+
;local-stats = local-stats
78+
;global-stats = global-stats
5479

5580
[sentry]
5681
dsn =
5782

5883
[uwsgi]
84+
;uid=pypi
85+
;gid=pypi
5986
wsgi-file = pypi.wsgi
6087
socket = /tmp/pypi.sock
88+
;pidfile = /var/run/devpypi/pypi.pid
89+
;daemonize = 127.0.0.1:8224
90+
;processes = 2
6191
harakiri = 60
92+
;reload-on-as = 400
93+
;max-requests = 10000
6294
master = 1
6395
post-buffering = 8192
6496
chmod-socket = 666
97+
;disable-logging = true
98+
;log-5xx = true
6599

100+
; CDN API
66101
[fastly]
67102
api_domain = https://api.fastly.com/
68103
api_key =

config.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def __init__(self, configfile):
4343
self.package_docs_url = c.get('webui', 'package_docs_url')
4444
else:
4545
self.package_docs_url = 'http://pythonhosted.org'
46-
self.mailhost = c.get('webui', 'mailhost')
4746
self.adminemail = c.get('webui', 'adminemail')
4847
self.replyto = c.get('webui', 'replyto')
4948
self.url = c.get('webui', 'url')
@@ -67,7 +66,7 @@ def __init__(self, configfile):
6766
self.reset_secret = c.get('webui', 'reset_secret')
6867

6968
self.logfile = c.get('logging', 'file')
70-
self.logging_mailhost = c.get('logging', 'mailhost')
69+
self.mail_logger = c.get('logging', 'mail_logger')
7170
self.fromaddr = c.get('logging', 'fromaddr')
7271
self.toaddrs = c.get('logging', 'toaddrs').split(',')
7372

@@ -89,6 +88,14 @@ def __init__(self, configfile):
8988
self.fastly_api_key = c.get("fastly", "api_key")
9089
self.fastly_service_id = c.get("fastly", "service_id")
9190

91+
# Get the smtp configuration
92+
self.smtp_hostname = c.get("smtp", "hostname")
93+
self.smtp_auth = c.get("smtp", "auth")
94+
self.smtp_starttls = c.get("smtp", "starttls")
95+
if self.smtp_auth:
96+
self.smtp_login = c.get("smtp", "login")
97+
self.smtp_password = c.get("smtp", "password")
98+
9299
def make_https(self):
93100
if self.url.startswith("http:"):
94101
self.url = "https"+self.url[4:]

tools/email_renamed_users.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@
7575
sent = []
7676

7777
# Email each user
78-
server = smtplib.SMTP(config.mailhost)
78+
server = smtplib.SMTP(config.mailgun_hostname)
79+
if config.smtp_starttls:
80+
server.starttls()
81+
if config.smtp_auth:
82+
server.login(config.smtp_login, config.smtp_password)
7983
for username, packages in users.iteritems():
8084
packages = sorted(set(packages))
8185

tools/hosting_mode_migration.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@
102102
sent = []
103103

104104
# Email each user
105-
server = smtplib.SMTP(config.mailhost)
105+
server = smtplib.SMTP(config.smtp_hostname)
106+
if config.smtp_starttls:
107+
server.starttls()
108+
if config.smtp_auth:
109+
server.login(config.smtp_login, config.smtp_password)
106110
for i, (package, users) in enumerate(package_users.iteritems()):
107111
fpackage = store.find_package(package)
108112

webui.py

+23-10
Original file line numberDiff line numberDiff line change
@@ -289,21 +289,30 @@ def __init__(self, handler, env):
289289
self.url_path = path
290290

291291
# configure logging
292-
if self.config.logfile or self.config.mailhost:
292+
if self.config.logfile or self.config.mail_logger:
293293
root = logging.getLogger()
294-
hdlrs = []
295294
if self.config.logfile:
296295
hdlr = logging.FileHandler(self.config.logfile)
297296
formatter = logging.Formatter(
298297
'%(asctime)s %(name)s:%(levelname)s %(message)s')
299298
hdlr.setFormatter(formatter)
300-
hdlrs.append(hdlr)
301-
if self.config.logging_mailhost:
302-
hdlr = MailingLogger.MailingLogger(self.config.logging_mailhost,
303-
self.config.fromaddr, self.config.toaddrs,
304-
'[PyPI] %(line)s', False, flood_level=10)
305-
hdlrs.append(hdlr)
306-
root.handlers = hdlrs
299+
root.handlers.append(hdlr)
300+
if self.config.mail_logger:
301+
smtp_starttls = None
302+
if self.config.smtp_starttls:
303+
smtp_starttls = ()
304+
smtp_credentials = None
305+
if self.config.smtp_auth:
306+
smtp_credentials = (self.config.smtp_login, self.config.smtp_password)
307+
hdlr = MailingLogger.MailingLogger(self.config.smtp_hostname,
308+
self.config.fromaddr,
309+
self.config.toaddrs,
310+
'[PyPI] %(line)s',
311+
credentials=smtp_credentials,
312+
secure=smtp_starttls,
313+
send_empty_entries=False,
314+
flood_level=10)
315+
root.handlers.append(hdlr)
307316

308317
def run(self):
309318
''' Run the request, handling all uncaught errors and finishing off
@@ -3172,7 +3181,11 @@ def delete_user(self):
31723181
def send_email(self, recipient, message):
31733182
''' Send an administrative email to the recipient
31743183
'''
3175-
smtp = smtplib.SMTP(self.config.mailhost)
3184+
smtp = smtplib.SMTP(self.config.smtp_hostname)
3185+
if self.config.smtp_starttls:
3186+
smtp.starttls()
3187+
if self.config.smtp_auth:
3188+
smtp.login(self.config.smtp_login, self.config.smtp_password)
31763189
smtp.sendmail(self.config.adminemail, recipient, message)
31773190

31783191
def packageURL(self, name, version):

0 commit comments

Comments
 (0)