Permalink
Browse files

Fix mailer so it can replace the sender if unauthorized.

And refactor the mailer into more OO style.
  • Loading branch information...
1 parent e6d56da commit 9c7cfebf47e929b71fd170d7594c1d16b55e2cb7 Toby White committed Jul 8, 2010
Showing with 116 additions and 82 deletions.
  1. +7 −2 gmail.py
  2. +109 −80 mail.py
View
@@ -51,14 +51,17 @@ def make_request(self, data):
class GmailProxy(object):
- def __init__(self, SECRET_KEY=None, EMAIL_APPENGINE_PROXY_URL=None, fail_silently=False):
+ def __init__(self, SECRET_KEY=None, EMAIL_APPENGINE_PROXY_URL=None, fix_sender=False, fail_silently=False):
self.signer = Signer(SECRET_KEY)
self.connection = Connection(EMAIL_APPENGINE_PROXY_URL)
+ self.fix_sender = fix_sender
self.fail_silently = fail_silently
def send_mail(self, msg):
values = {'msg':msg.as_string(),
'signature':self.signer.generate_signature(msg.as_string())}
+ if self.fix_sender:
+ values['fix_sender'] = 'true'
data = urllib.urlencode([(k, v.encode('utf-8')) for k, v in values.items()])
status, errmsg = self.connection.make_request(data)
@@ -71,6 +74,8 @@ def send_mail(self, msg):
and the message on stdin"""
parser = optparse.OptionParser()
parser.add_option("-s", dest="subject", help="subject of message")
+ parser.add_option("--fix-sender", action="store_true", dest="fix_sender",
+ help="If sender is not authorized, replace From with an authorized sender")
options, to_addresses = parser.parse_args()
if to_addresses:
msg = email.message.Message()
@@ -84,4 +89,4 @@ def send_mail(self, msg):
recipient = os.environ.get('RECIPIENT')
if recipient:
msg['To'] = recipient
- GmailProxy().send_mail(msg)
+ GmailProxy(fix_sender=options.fix_sender).send_mail(msg)
View
@@ -17,96 +17,109 @@ class BadMessageError(ValueError):
pass
-def get_filename(part):
- filename = part.get_filename()
- if not filename:
- content_type = part.get_content_type()
+class Mailer(object):
+ def __init__(self, default_sender, fix_sender=False):
+ self.default_sender = default_sender
+ self.fix_sender = fix_sender
+
+ def send_message(self, msg):
+ message = self.translate_message(msg)
+ try:
+ message.send()
+ return
+ except InvalidSenderError:
+ if not self.fix_sender:
+ raise BadMessageError("Unauthorized message sender '%s'" % sender)
+ message.sender = self.default_sender
try:
- filename = "file.%s" % suffixes[content_type]
- except KeyError:
- raise BadMessageError("Google won't let us send content of type '%s'" % content_type)
- return filename
-
-def send_message(msg):
- sender = msg.get_unixfrom() or msg['From']
- if not sender:
- raise BadMessageError("No sender specified")
- to = msg['To']
- if not to:
- raise BadMessageError("No destination addresses specified")
- message = EmailMessage(sender=sender or msg['From'], to=to)
-
- # Go through all the headers which Google will let us use
- cc = msg['Cc']
- if cc:
- message.cc = cc
- bcc = msg['Bcc']
- if bcc:
- message.bcc = cc
- reply_to = msg['Reply-To']
- if reply_to:
- message.reply_to = reply_to
- subject = msg['Subject']
- if subject:
- message.subject = subject
-
- # If there's just a plain text body, use that, otherwise
- # iterate over all the attachments
- payload = msg.get_payload()
- if isinstance(payload, basestring):
- message.body = payload
- else:
- body = ''
- html = ''
- attachments = []
- # GAE demands we specify the body explicitly - we use the first text/plain attachment we find.
- # Similarly, we pull in the first html we find and use that for message.html
- # We pull in any other attachments we find; but we ignore the multipart structure,
- # because GAE doesn't give us enough control there.
- for part in msg.walk():
- if part.get_content_type() == 'text/plain' and not body:
- body = part.get_payload(decode=True)
- elif part.get_content_type() == 'text/html' and not html:
- html = part.get_payload(decode=True)
- elif not part.get_content_type().startswith('multipart'):
- attachments.append((get_filename(part), part.get_payload(decode=True)))
- if not body:
- raise BadMessageError("No message body specified")
- message.body = body
- if html:
- message.html = html
- if attachments:
- message.attachments = attachments
- try:
- message.send()
- except InvalidSenderError:
- raise BadMessageError("Unauthorized message sender '%s'" % sender)
-
-def check_signature(msg, signature):
- GMAIL_SECRET_KEYS = [k.strip() for k in open('GMAIL_SECRET_KEYS') if k]
- return Signer(GMAIL_SECRET_KEYS).verify_signature(msg, signature)
-
-def parse_args(request):
- msg = request.get('msg')
- if not msg:
- raise BadRequestError("No message found")
- signature = request.get('signature')
- if not signature:
- raise BadRequestError("No signature found")
- if not check_signature(msg, signature):
- raise BadRequestError("Signature doesn't match")
- return str(msg) # email.parser barfs on unicode
+ message.send()
+ except InvalidSenderError:
+ raise BadMessageError("Unauthorized default message sender '%s'" % sender)
+
+ @staticmethod
+ def get_filename(part):
+ filename = part.get_filename()
+ if not filename:
+ content_type = part.get_content_type()
+ try:
+ filename = "file.%s" % suffixes[content_type]
+ except KeyError:
+ raise BadMessageError("Google won't let us send content of type '%s'" % content_type)
+ return filename
+
+ def translate_message(self, msg):
+ sender = msg.get_unixfrom() or msg['From']
+ if not sender:
+ if self.fix_sender:
+ sender = self.default_sender
+ else:
+ raise BadMessageError("No sender specified")
+ to = msg['To']
+ if not to:
+ raise BadMessageError("No destination addresses specified")
+ message = EmailMessage(sender=sender or msg['From'], to=to)
+ # Go through all the headers which Google will let us use
+ cc = msg['Cc']
+ if cc:
+ message.cc = cc
+ bcc = msg['Bcc']
+ if bcc:
+ message.bcc = cc
+ reply_to = msg['Reply-To']
+ if reply_to:
+ message.reply_to = reply_to
+ subject = msg['Subject']
+ if subject:
+ message.subject = subject
+
+ # If there's just a plain text body, use that, otherwise
+ # iterate over all the attachments
+ payload = msg.get_payload()
+ if isinstance(payload, basestring):
+ message.body = payload
+ else:
+ body = ''
+ html = ''
+ attachments = []
+ # GAE demands we specify the body explicitly - we use the first text/plain attachment we find.
+ # Similarly, we pull in the first html we find and use that for message.html
+ # We pull in any other attachments we find; but we ignore the multipart structure,
+ # because GAE doesn't give us enough control there.
+ for part in msg.walk():
+ if part.get_content_type() == 'text/plain' and not body:
+ body = part.get_payload(decode=True)
+ elif part.get_content_type() == 'text/html' and not html:
+ html = part.get_payload(decode=True)
+ elif not part.get_content_type().startswith('multipart'):
+ attachments.append((get_filename(part), part.get_payload(decode=True)))
+ if not body:
+ raise BadMessageError("No message body specified")
+ message.body = body
+ if html:
+ message.html = html
+ if attachments:
+ message.attachments = attachments
+ return message
class SendMail(RequestHandler):
+ def __init__(self, *args, **kwargs):
+ self.GMAIL_SECRET_KEYS = [k.strip() for k in
+ open('GMAIL_SECRET_KEYS')
+ if k]
+ self.default_sender = open('GMAIL_DEFAULT_SENDER').read().strip()
+ super(SendMail, self).__init__(*args, **kwargs)
+
def get(self):
# Just so that we can pingdom it to see if it's up.
return
def post(self):
try:
- msg = parse_args(self.request)
- send_message(email.parser.Parser().parsestr(msg))
+ msg_string, fix_sender = self.parse_args()
+ msg = email.parser.Parser().parsestr(msg_string)
+ mailer = Mailer(self.default_sender, fix_sender)
+ mailer.send_message(msg)
logging.info("Sent message ok\n%s" % msg)
self.error(204)
except BadRequestError, e:
@@ -120,3 +133,19 @@ def post(self):
except Exception, e:
logging.exception("Failed to process request")
self.error(500)
+
+ def parse_args(self):
+ msg = self.request.get('msg')
+ if not msg:
+ raise BadRequestError("No message found")
+ signature = self.request.get('signature')
+ if not signature:
+ raise BadRequestError("No signature found")
+ if not self.check_signature(msg, signature):
+ raise BadRequestError("Signature doesn't match")
+ msg = str(msg) # email.parser fails on unicode
+ fix_sender = bool(self.request.get("fix_sender"))
+ return msg, fix_sender
+
+ def check_signature(self, msg, signature):
+ return Signer(self.GMAIL_SECRET_KEYS).verify_signature(msg, signature)

0 comments on commit 9c7cfeb

Please sign in to comment.