Include original error in debug log
[mediagoblin.git] / mediagoblin / tools / mail.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 from __future__ import print_function, unicode_literals
18
19 import socket
20 import logging
21 import six
22 import smtplib
23 import sys
24 from mediagoblin import mg_globals, messages
25 from mediagoblin._compat import MIMEText
26 from mediagoblin.tools import common
27
28 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29 ### Special email test stuff begins HERE
30 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31
32 # We have two "test inboxes" here:
33 #
34 # EMAIL_TEST_INBOX:
35 # ----------------
36 # If you're writing test views, you'll probably want to check this.
37 # It contains a list of MIMEText messages.
38 #
39 # EMAIL_TEST_MBOX_INBOX:
40 # ----------------------
41 # This collects the messages from the FakeMhost inbox. It's reslly
42 # just here for testing the send_email method itself.
43 #
44 # Anyway this contains:
45 # - from
46 # - to: a list of email recipient addresses
47 # - message: not just the body, but the whole message, including
48 # headers, etc.
49 #
50 # ***IMPORTANT!***
51 # ----------------
52 # Before running tests that call functions which send email, you should
53 # always call _clear_test_inboxes() to "wipe" the inboxes clean.
54
55 EMAIL_TEST_INBOX = []
56 EMAIL_TEST_MBOX_INBOX = []
57
58
59 class MailError(Exception):
60 """ General exception for mail errors """
61
62
63 class NoSMTPServerError(MailError):
64 pass
65
66
67 class FakeMhost(object):
68 """
69 Just a fake mail host so we can capture and test messages
70 from send_email
71 """
72 def login(self, *args, **kwargs):
73 pass
74
75 def sendmail(self, from_addr, to_addrs, message):
76 EMAIL_TEST_MBOX_INBOX.append(
77 {'from': from_addr,
78 'to': to_addrs,
79 'message': message})
80
81 def starttls(self):
82 raise smtplib.SMTPException("No STARTTLS here")
83
84 def _clear_test_inboxes():
85 global EMAIL_TEST_INBOX
86 global EMAIL_TEST_MBOX_INBOX
87 EMAIL_TEST_INBOX = []
88 EMAIL_TEST_MBOX_INBOX = []
89
90
91 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
92 ### </Special email test stuff>
93 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
94
95 def send_email(from_addr, to_addrs, subject, message_body):
96 """
97 Simple email sending wrapper, use this so we can capture messages
98 for unit testing purposes.
99
100 Args:
101 - from_addr: address you're sending the email from
102 - to_addrs: list of recipient email addresses
103 - subject: subject of the email
104 - message_body: email body text
105 """
106 if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']:
107 mhost = FakeMhost()
108 elif not mg_globals.app_config['email_debug_mode']:
109 if mg_globals.app_config['email_smtp_use_ssl']:
110 smtp_init = smtplib.SMTP_SSL
111 else:
112 smtp_init = smtplib.SMTP
113
114 try:
115 mhost = smtp_init(
116 mg_globals.app_config['email_smtp_host'],
117 mg_globals.app_config['email_smtp_port'])
118 except socket.error as original_error:
119 error_message = "Couldn't contact mail server on <{}>:<{}>".format(
120 mg_globals.app_config['email_smtp_host'],
121 mg_globals.app_config['email_smtp_port'])
122 logging.debug(original_error)
123 raise NoSMTPServerError(error_message)
124
125 # SMTP.__init__ Issues SMTP.connect implicitly if host
126 if not mg_globals.app_config['email_smtp_host']: # e.g. host = ''
127 try:
128 mhost.connect() # We SMTP.connect explicitly
129 except socket.error as original_error:
130 error_message = "Couldn't contact mail server on <{}>:<{}>".format(
131 mg_globals.app_config['email_smtp_host'],
132 mg_globals.app_config['email_smtp_port'])
133 logging.debug(original_error)
134 raise NoSMTPServerError(error_message)
135
136 try:
137 mhost.starttls()
138 except smtplib.SMTPException:
139 # Only raise an exception if we're forced to
140 if mg_globals.app_config['email_smtp_force_starttls']:
141 six.reraise(*sys.exc_info())
142
143 if ((not common.TESTS_ENABLED)
144 and (mg_globals.app_config['email_smtp_user']
145 or mg_globals.app_config['email_smtp_pass'])):
146 mhost.login(
147 mg_globals.app_config['email_smtp_user'],
148 mg_globals.app_config['email_smtp_pass'])
149
150 message = MIMEText(message_body.encode('utf-8'), 'plain', 'utf-8')
151 message['Subject'] = subject
152 message['From'] = from_addr
153 message['To'] = ', '.join(to_addrs)
154
155 if common.TESTS_ENABLED:
156 EMAIL_TEST_INBOX.append(message)
157
158 elif mg_globals.app_config['email_debug_mode']:
159 print("===== Email =====")
160 print("From address: %s" % message['From'])
161 print("To addresses: %s" % message['To'])
162 print("Subject: %s" % message['Subject'])
163 print("-- Body: --")
164 print(message_body)
165
166 return mhost.sendmail(from_addr, to_addrs, message.as_string())
167
168
169 def normalize_email(email):
170 """return case sensitive part, lower case domain name
171
172 :returns: None in case of broken email addresses"""
173 try:
174 em_user, em_dom = email.split('@', 1)
175 except ValueError:
176 # email contained no '@'
177 return None
178 email = "@".join((em_user, em_dom.lower()))
179 return email
180
181
182 def email_debug_message(request):
183 """
184 If the server is running in email debug mode (which is
185 the current default), give a debug message to the user
186 so that they have an idea where to find their email.
187 """
188 if mg_globals.app_config['email_debug_mode']:
189 # DEBUG message, no need to translate
190 messages.add_message(
191 request,
192 messages.DEBUG,
193 "This instance is running in email debug mode. "
194 "The email will be on the console of the server process.")