Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Unified Diff: appengine/monorail/features/inboundemail.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « appengine/monorail/features/filterrules_views.py ('k') | appengine/monorail/features/notify.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: appengine/monorail/features/inboundemail.py
diff --git a/appengine/monorail/features/inboundemail.py b/appengine/monorail/features/inboundemail.py
new file mode 100644
index 0000000000000000000000000000000000000000..b25b3128a1b2b752b90513eee81a6193c68eedd2
--- /dev/null
+++ b/appengine/monorail/features/inboundemail.py
@@ -0,0 +1,258 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is govered by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Handler to process inbound email with issue comments and commands."""
+
+import logging
+import os
+import urllib
+
+from third_party import ezt
+
+from google.appengine.api import mail
+
+import webapp2
+
+from features import commitlogcommands
+from features import notify
+from framework import emailfmt
+from framework import framework_constants
+from framework import monorailrequest
+from framework import permissions
+from framework import sql
+from framework import template_helpers
+from proto import project_pb2
+from services import issue_svc
+from services import user_svc
+
+
+TEMPLATE_PATH_BASE = framework_constants.TEMPLATE_PATH
+
+MSG_TEMPLATES = {
+ 'banned': 'features/inboundemail-banned.ezt',
+ 'body_too_long': 'features/inboundemail-body-too-long.ezt',
+ 'project_not_found': 'features/inboundemail-project-not-found.ezt',
+ 'not_a_reply': 'features/inboundemail-not-a-reply.ezt',
+ 'no_account': 'features/inboundemail-no-account.ezt',
+ 'no_artifact': 'features/inboundemail-no-artifact.ezt',
+ 'no_perms': 'features/inboundemail-no-perms.ezt',
+ 'replies_disabled': 'features/inboundemail-replies-disabled.ezt',
+ }
+
+
+class InboundEmail(webapp2.RequestHandler):
+ """Servlet to handle inbound email messages."""
+
+ def __init__(self, request, response, services=None, *args, **kwargs):
+ super(InboundEmail, self).__init__(request, response, *args, **kwargs)
+ self.services = services or self.app.config.get('services')
+ self._templates = {}
+ for name, template_path in MSG_TEMPLATES.iteritems():
+ self._templates[name] = template_helpers.MonorailTemplate(
+ TEMPLATE_PATH_BASE + template_path,
+ compress_whitespace=False, base_format=ezt.FORMAT_RAW)
+
+ def get(self, project_addr=None):
+ logging.info('\n\n\nGET for InboundEmail and project_addr is %r',
+ project_addr)
+ self.Handler(mail.InboundEmailMessage(self.request.body),
+ urllib.unquote(project_addr))
+
+ def post(self, project_addr=None):
+ logging.info('\n\n\nPOST for InboundEmail and project_addr is %r',
+ project_addr)
+ self.Handler(mail.InboundEmailMessage(self.request.body),
+ urllib.unquote(project_addr))
+
+ def Handler(self, inbound_email_message, project_addr):
+ """Process an inbound email message."""
+ msg = inbound_email_message.original
+ email_tasks = self.ProcessMail(msg, project_addr)
+
+ if email_tasks:
+ notify.AddAllEmailTasks(email_tasks)
+
+ def ProcessMail(self, msg, project_addr):
+ """Process an inbound email message."""
+ # TODO(jrobbins): If the message is HUGE, don't even try to parse
+ # it. Silently give up.
+
+ (from_addr, to_addrs, cc_addrs, references, subject,
+ body) = emailfmt.ParseEmailMessage(msg)
+
+ logging.info('Proj addr: %r', project_addr)
+ logging.info('From addr: %r', from_addr)
+ logging.info('Subject: %r', subject)
+ logging.info('To: %r', to_addrs)
+ logging.info('Cc: %r', cc_addrs)
+ logging.info('References: %r', references)
+ logging.info('Body: %r', body)
+
+ # If message body is very large, reject it and send an error email.
+ if emailfmt.IsBodyTooBigToParse(body):
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['body_too_long'])
+
+ # Make sure that the project reply-to address is in the To: line.
+ if not emailfmt.IsProjectAddressOnToLine(project_addr, to_addrs):
+ return None
+
+ # Identify the project and artifact to update.
+ project_name, local_id = emailfmt.IdentifyProjectAndIssue(
+ project_addr, subject)
+ if not project_addr or not local_id:
+ logging.info('Could not identify issue: %s %s', project_addr, subject)
+ # No error message, because message was probably not intended for us.
+ return None
+
+ cnxn = sql.MonorailConnection()
+ if self.services.cache_manager:
+ self.services.cache_manager.DoDistributedInvalidation(cnxn)
+
+ project = self.services.project.GetProjectByName(cnxn, project_name)
+
+ if not project or project.state != project_pb2.ProjectState.LIVE:
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['project_not_found'])
+
+ if not project.process_inbound_email:
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['replies_disabled'],
+ project_name=project_name)
+
+ # Verify that this is a reply to a notification that we could have sent.
+ if not os.environ['SERVER_SOFTWARE'].startswith('Development'):
+ for ref in references:
+ if emailfmt.ValidateReferencesHeader(ref, project, from_addr, subject):
+ break # Found a message ID that we could have sent.
+ else:
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['not_a_reply'])
+
+ # Authenticate the from-addr and perm check.
+ # Note: If the issue summary line is changed, a new thread is created,
+ # and replies to the old thread will no longer work because the subject
+ # line hash will not match, which seems reasonable.
+ try:
+ auth = monorailrequest.AuthData.FromEmail(cnxn, from_addr, self.services)
+ from_user_id = auth.user_id
+ except user_svc.NoSuchUserException:
+ from_user_id = None
+ if not from_user_id:
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['no_account'])
+
+ if auth.user_pb.banned:
+ logging.info('Banned user %s tried to post to %s',
+ from_addr, project_addr)
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['banned'])
+
+ perms = permissions.GetPermissions(
+ auth.user_pb, auth.effective_ids, project)
+
+ self.ProcessIssueReply(
+ cnxn, project, local_id, project_addr, from_addr, from_user_id,
+ auth.effective_ids, perms, body)
+
+ return None
+
+ def ProcessIssueReply(
+ self, cnxn, project, local_id, project_addr, from_addr, from_user_id,
+ effective_ids, perms, body):
+ """Examine an issue reply email body and add a comment to the issue.
+
+ Args:
+ cnxn: connection to SQL database.
+ project: Project PB for the project containing the issue.
+ local_id: int ID of the issue being replied to.
+ project_addr: string email address used for outbound emails from
+ that project.
+ from_addr: string email address of the user who sent the email
+ reply to our server.
+ from_user_id: int user ID of user who sent the reply email.
+ effective_ids: set of int user IDs for the user (including any groups),
+ or an empty set if user is not signed in.
+ perms: PermissionSet for the user who sent the reply email.
+ body: string email body text of the reply email.
+
+ Returns:
+ A list of follow-up work items, e.g., to notify other users of
+ the new comment, or to notify the user that their reply was not
+ processed.
+
+ Side-effect:
+ Adds a new comment to the issue, if no error is reported.
+ """
+ try:
+ issue = self.services.issue.GetIssueByLocalID(
+ cnxn, project.project_id, local_id)
+ except issue_svc.NoSuchIssueException:
+ issue = None
+
+ if not issue or issue.deleted:
+ # The referenced issue was not found, e.g., it might have been
+ # deleted, or someone messed with the subject line. Reject it.
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['no_artifact'],
+ artifact_phrase='issue %d' % local_id,
+ project_name=project.project_name)
+
+ if not perms.CanUsePerm(
+ permissions.ADD_ISSUE_COMMENT, effective_ids, project,
+ permissions.GetRestrictions(issue)):
+ return _MakeErrorMessageReplyTask(
+ project_addr, from_addr, self._templates['no_perms'],
+ artifact_phrase='issue %d' % local_id,
+ project_name=project.project_name)
+ allow_edit = permissions.CanEditIssue(
+ effective_ids, perms, project, issue)
+ # TODO(jrobbins): if the user does not have EDIT_ISSUE and the inbound
+ # email tries to make an edit, send back an error message.
+
+ lines = body.strip().split('\n')
+ uia = commitlogcommands.UpdateIssueAction(local_id)
+ uia.Parse(cnxn, project.project_name, from_user_id, lines, self.services,
+ strip_quoted_lines=True)
+ uia.Run(cnxn, self.services, allow_edit=allow_edit)
+
+
+def _MakeErrorMessageReplyTask(
+ project_addr, sender_addr, template, **callers_page_data):
+ """Return a new task to send an error message email.
+
+ Args:
+ project_addr: string email address that the inbound email was delivered to.
+ sender_addr: string email address of user who sent the email that we could
+ not process.
+ template: EZT template used to generate the email error message. The
+ first line of this generated text will be used as the subject line.
+ callers_page_data: template data dict for body of the message.
+
+ Returns:
+ A list with a single Email task that can be enqueued to
+ actually send the email.
+
+ Raises:
+ ValueError: if the template does begin with a "Subject:" line.
+ """
+ email_data = {
+ 'project_addr': project_addr,
+ 'sender_addr': sender_addr
+ }
+ email_data.update(callers_page_data)
+
+ generated_lines = template.GetResponse(email_data)
+ subject, body = generated_lines.split('\n', 1)
+ if subject.startswith('Subject: '):
+ subject = subject[len('Subject: '):]
+ else:
+ raise ValueError('Email template does not begin with "Subject:" line.')
+
+ email_task = dict(to=sender_addr, subject=subject, body=body,
+ from_addr=emailfmt.NoReplyAddress())
+ logging.info('sending email error reply: %r', email_task)
+
+ return [email_task]
« no previous file with comments | « appengine/monorail/features/filterrules_views.py ('k') | appengine/monorail/features/notify.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698