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

Side by Side 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 unified diff | 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered by a BSD-style
3 # license that can be found in the LICENSE file or at
4 # https://developers.google.com/open-source/licenses/bsd
5
6 """Handler to process inbound email with issue comments and commands."""
7
8 import logging
9 import os
10 import urllib
11
12 from third_party import ezt
13
14 from google.appengine.api import mail
15
16 import webapp2
17
18 from features import commitlogcommands
19 from features import notify
20 from framework import emailfmt
21 from framework import framework_constants
22 from framework import monorailrequest
23 from framework import permissions
24 from framework import sql
25 from framework import template_helpers
26 from proto import project_pb2
27 from services import issue_svc
28 from services import user_svc
29
30
31 TEMPLATE_PATH_BASE = framework_constants.TEMPLATE_PATH
32
33 MSG_TEMPLATES = {
34 'banned': 'features/inboundemail-banned.ezt',
35 'body_too_long': 'features/inboundemail-body-too-long.ezt',
36 'project_not_found': 'features/inboundemail-project-not-found.ezt',
37 'not_a_reply': 'features/inboundemail-not-a-reply.ezt',
38 'no_account': 'features/inboundemail-no-account.ezt',
39 'no_artifact': 'features/inboundemail-no-artifact.ezt',
40 'no_perms': 'features/inboundemail-no-perms.ezt',
41 'replies_disabled': 'features/inboundemail-replies-disabled.ezt',
42 }
43
44
45 class InboundEmail(webapp2.RequestHandler):
46 """Servlet to handle inbound email messages."""
47
48 def __init__(self, request, response, services=None, *args, **kwargs):
49 super(InboundEmail, self).__init__(request, response, *args, **kwargs)
50 self.services = services or self.app.config.get('services')
51 self._templates = {}
52 for name, template_path in MSG_TEMPLATES.iteritems():
53 self._templates[name] = template_helpers.MonorailTemplate(
54 TEMPLATE_PATH_BASE + template_path,
55 compress_whitespace=False, base_format=ezt.FORMAT_RAW)
56
57 def get(self, project_addr=None):
58 logging.info('\n\n\nGET for InboundEmail and project_addr is %r',
59 project_addr)
60 self.Handler(mail.InboundEmailMessage(self.request.body),
61 urllib.unquote(project_addr))
62
63 def post(self, project_addr=None):
64 logging.info('\n\n\nPOST for InboundEmail and project_addr is %r',
65 project_addr)
66 self.Handler(mail.InboundEmailMessage(self.request.body),
67 urllib.unquote(project_addr))
68
69 def Handler(self, inbound_email_message, project_addr):
70 """Process an inbound email message."""
71 msg = inbound_email_message.original
72 email_tasks = self.ProcessMail(msg, project_addr)
73
74 if email_tasks:
75 notify.AddAllEmailTasks(email_tasks)
76
77 def ProcessMail(self, msg, project_addr):
78 """Process an inbound email message."""
79 # TODO(jrobbins): If the message is HUGE, don't even try to parse
80 # it. Silently give up.
81
82 (from_addr, to_addrs, cc_addrs, references, subject,
83 body) = emailfmt.ParseEmailMessage(msg)
84
85 logging.info('Proj addr: %r', project_addr)
86 logging.info('From addr: %r', from_addr)
87 logging.info('Subject: %r', subject)
88 logging.info('To: %r', to_addrs)
89 logging.info('Cc: %r', cc_addrs)
90 logging.info('References: %r', references)
91 logging.info('Body: %r', body)
92
93 # If message body is very large, reject it and send an error email.
94 if emailfmt.IsBodyTooBigToParse(body):
95 return _MakeErrorMessageReplyTask(
96 project_addr, from_addr, self._templates['body_too_long'])
97
98 # Make sure that the project reply-to address is in the To: line.
99 if not emailfmt.IsProjectAddressOnToLine(project_addr, to_addrs):
100 return None
101
102 # Identify the project and artifact to update.
103 project_name, local_id = emailfmt.IdentifyProjectAndIssue(
104 project_addr, subject)
105 if not project_addr or not local_id:
106 logging.info('Could not identify issue: %s %s', project_addr, subject)
107 # No error message, because message was probably not intended for us.
108 return None
109
110 cnxn = sql.MonorailConnection()
111 if self.services.cache_manager:
112 self.services.cache_manager.DoDistributedInvalidation(cnxn)
113
114 project = self.services.project.GetProjectByName(cnxn, project_name)
115
116 if not project or project.state != project_pb2.ProjectState.LIVE:
117 return _MakeErrorMessageReplyTask(
118 project_addr, from_addr, self._templates['project_not_found'])
119
120 if not project.process_inbound_email:
121 return _MakeErrorMessageReplyTask(
122 project_addr, from_addr, self._templates['replies_disabled'],
123 project_name=project_name)
124
125 # Verify that this is a reply to a notification that we could have sent.
126 if not os.environ['SERVER_SOFTWARE'].startswith('Development'):
127 for ref in references:
128 if emailfmt.ValidateReferencesHeader(ref, project, from_addr, subject):
129 break # Found a message ID that we could have sent.
130 else:
131 return _MakeErrorMessageReplyTask(
132 project_addr, from_addr, self._templates['not_a_reply'])
133
134 # Authenticate the from-addr and perm check.
135 # Note: If the issue summary line is changed, a new thread is created,
136 # and replies to the old thread will no longer work because the subject
137 # line hash will not match, which seems reasonable.
138 try:
139 auth = monorailrequest.AuthData.FromEmail(cnxn, from_addr, self.services)
140 from_user_id = auth.user_id
141 except user_svc.NoSuchUserException:
142 from_user_id = None
143 if not from_user_id:
144 return _MakeErrorMessageReplyTask(
145 project_addr, from_addr, self._templates['no_account'])
146
147 if auth.user_pb.banned:
148 logging.info('Banned user %s tried to post to %s',
149 from_addr, project_addr)
150 return _MakeErrorMessageReplyTask(
151 project_addr, from_addr, self._templates['banned'])
152
153 perms = permissions.GetPermissions(
154 auth.user_pb, auth.effective_ids, project)
155
156 self.ProcessIssueReply(
157 cnxn, project, local_id, project_addr, from_addr, from_user_id,
158 auth.effective_ids, perms, body)
159
160 return None
161
162 def ProcessIssueReply(
163 self, cnxn, project, local_id, project_addr, from_addr, from_user_id,
164 effective_ids, perms, body):
165 """Examine an issue reply email body and add a comment to the issue.
166
167 Args:
168 cnxn: connection to SQL database.
169 project: Project PB for the project containing the issue.
170 local_id: int ID of the issue being replied to.
171 project_addr: string email address used for outbound emails from
172 that project.
173 from_addr: string email address of the user who sent the email
174 reply to our server.
175 from_user_id: int user ID of user who sent the reply email.
176 effective_ids: set of int user IDs for the user (including any groups),
177 or an empty set if user is not signed in.
178 perms: PermissionSet for the user who sent the reply email.
179 body: string email body text of the reply email.
180
181 Returns:
182 A list of follow-up work items, e.g., to notify other users of
183 the new comment, or to notify the user that their reply was not
184 processed.
185
186 Side-effect:
187 Adds a new comment to the issue, if no error is reported.
188 """
189 try:
190 issue = self.services.issue.GetIssueByLocalID(
191 cnxn, project.project_id, local_id)
192 except issue_svc.NoSuchIssueException:
193 issue = None
194
195 if not issue or issue.deleted:
196 # The referenced issue was not found, e.g., it might have been
197 # deleted, or someone messed with the subject line. Reject it.
198 return _MakeErrorMessageReplyTask(
199 project_addr, from_addr, self._templates['no_artifact'],
200 artifact_phrase='issue %d' % local_id,
201 project_name=project.project_name)
202
203 if not perms.CanUsePerm(
204 permissions.ADD_ISSUE_COMMENT, effective_ids, project,
205 permissions.GetRestrictions(issue)):
206 return _MakeErrorMessageReplyTask(
207 project_addr, from_addr, self._templates['no_perms'],
208 artifact_phrase='issue %d' % local_id,
209 project_name=project.project_name)
210 allow_edit = permissions.CanEditIssue(
211 effective_ids, perms, project, issue)
212 # TODO(jrobbins): if the user does not have EDIT_ISSUE and the inbound
213 # email tries to make an edit, send back an error message.
214
215 lines = body.strip().split('\n')
216 uia = commitlogcommands.UpdateIssueAction(local_id)
217 uia.Parse(cnxn, project.project_name, from_user_id, lines, self.services,
218 strip_quoted_lines=True)
219 uia.Run(cnxn, self.services, allow_edit=allow_edit)
220
221
222 def _MakeErrorMessageReplyTask(
223 project_addr, sender_addr, template, **callers_page_data):
224 """Return a new task to send an error message email.
225
226 Args:
227 project_addr: string email address that the inbound email was delivered to.
228 sender_addr: string email address of user who sent the email that we could
229 not process.
230 template: EZT template used to generate the email error message. The
231 first line of this generated text will be used as the subject line.
232 callers_page_data: template data dict for body of the message.
233
234 Returns:
235 A list with a single Email task that can be enqueued to
236 actually send the email.
237
238 Raises:
239 ValueError: if the template does begin with a "Subject:" line.
240 """
241 email_data = {
242 'project_addr': project_addr,
243 'sender_addr': sender_addr
244 }
245 email_data.update(callers_page_data)
246
247 generated_lines = template.GetResponse(email_data)
248 subject, body = generated_lines.split('\n', 1)
249 if subject.startswith('Subject: '):
250 subject = subject[len('Subject: '):]
251 else:
252 raise ValueError('Email template does not begin with "Subject:" line.')
253
254 email_task = dict(to=sender_addr, subject=subject, body=body,
255 from_addr=emailfmt.NoReplyAddress())
256 logging.info('sending email error reply: %r', email_task)
257
258 return [email_task]
OLDNEW
« 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