forked from ietf-tools/datatracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmail.py
More file actions
210 lines (184 loc) · 7.52 KB
/
mail.py
File metadata and controls
210 lines (184 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# Copyright The IETF Trust 2014-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import base64
import datetime
from dateutil.tz import tzoffset
import os
import re
from email import message_from_bytes
from email.utils import parsedate_tz
from django.template.loader import render_to_string
from django.utils.encoding import force_text, force_bytes
import debug # pyflakes:ignore
from ietf.ipr.models import IprEvent
from ietf.message.models import Message
from ietf.person.models import Person
from ietf.utils.log import log
from ietf.mailtrigger.utils import get_base_ipr_request_address
# ----------------------------------------------------------------
# Date Functions
# ----------------------------------------------------------------
def get_body(msg):
"""Returns the body of the message. A Basic routine to walk parts of a MIME message
concatenating text/plain parts"""
body = ''
for part in msg.walk():
if part.get_content_type() == 'text/plain':
body = body + part.get_payload() + '\n'
return body
def is_aware(date):
"""Returns True if the date object passed in timezone aware, False if naive.
See http://docs.python.org/2/library/datetime.html section 8.1.1
"""
if not isinstance(date,datetime.datetime):
return False
if date.tzinfo and date.tzinfo.utcoffset(date) is not None:
return True
return False
def parsedate_to_datetime(date):
"""Returns a datetime object from string. May return naive or aware datetime.
This function is from email standard library v3.3, converted to 2.x
http://python.readthedocs.org/en/latest/library/email.util.html
"""
try:
tuple = parsedate_tz(date)
if not tuple:
return None
tz = tuple[-1]
if tz is None:
return datetime.datetime(*tuple[:6])
return datetime.datetime(*tuple[:6],tzinfo=tzoffset(None,tz))
except ValueError:
return None
def utc_from_string(s):
date = parsedate_to_datetime(s)
if date is None:
return None
elif is_aware(date):
return date.astimezone(datetime.timezone.utc)
else:
return date.replace(tzinfo=datetime.timezone.utc)
# ----------------------------------------------------------------
# Email Functions
# ----------------------------------------------------------------
def get_holders(ipr):
"""Recursive function to follow chain of disclosure updates and return holder emails"""
items = []
for x in [ y.target.get_child() for y in ipr.updates]:
items.extend(get_holders(x))
return ([ipr.holder_contact_email] if hasattr(ipr,'holder_contact_email') else []) + items
def get_pseudo_submitter(ipr):
"""Returns a tuple (name, email) contact for this disclosure. Order of preference
is submitter, ietfer, holder (per legacy app)"""
name = 'UNKNOWN NAME - NEED ASSISTANCE HERE'
email = 'UNKNOWN EMAIL - NEED ASSISTANCE HERE'
if ipr.submitter_email:
name = ipr.submitter_name
email = ipr.submitter_email
elif hasattr(ipr, 'ietfer_contact_email') and ipr.ietfer_contact_email:
name = ipr.ietfer_name
email = ipr.ietfer_contact_email
elif hasattr(ipr, 'holder_contact_email') and ipr.holder_contact_email:
name = ipr.holder_contact_name
email = ipr.holder_contact_email
return (name,email)
def get_reply_to():
"""Returns a new reply-to address for use with an outgoing message. This is an
address with "plus addressing" using a random string. Guaranteed to be unique"""
local,domain = get_base_ipr_request_address().split('@')
while True:
rand = force_text(base64.urlsafe_b64encode(os.urandom(12)))
address = "{}+{}@{}".format(local,rand,domain)
q = Message.objects.filter(reply_to=address)
if not q:
break
return address
def get_update_cc_addrs(ipr):
"""Returns list (as a string) of email addresses to use in CC: for an IPR update.
Logic is from legacy tool. Append submitter or ietfer email of first-order updated
IPR, append holder of updated IPR, follow chain of updates, appending holder emails
"""
emails = []
if not ipr.updates:
return ''
for rel in ipr.updates:
if rel.target.submitter_email:
emails.append(rel.target.submitter_email)
elif hasattr(rel.target,'ietfer_email') and rel.target.ietfer_email:
emails.append(rel.target.ietfer_email)
emails = emails + get_holders(ipr)
return ','.join(list(set(emails)))
def get_update_submitter_emails(ipr):
"""Returns list of messages, as flat strings, to submitters of IPR(s) being
updated"""
messages = []
email_to_iprs = {}
email_to_name = {}
for related in ipr.updates:
name, email = get_pseudo_submitter(related.target)
email_to_name[email] = name
if email in email_to_iprs:
email_to_iprs[email].append(related.target)
else:
email_to_iprs[email] = [related.target]
# TODO: This has not been converted to use mailtrigger. It is complicated.
# When converting it, it will need something like ipr_submitter_ietfer_or_holder perhaps
for email in email_to_iprs:
context = dict(
to_email=email,
to_name=email_to_name[email],
iprs=email_to_iprs[email],
new_ipr=ipr,
reply_to=get_reply_to())
text = render_to_string('ipr/update_submitter_email.txt',context)
messages.append(text)
return messages
def message_from_message(message,by=None):
"""Returns a ietf.message.models.Message. msg=email.Message"""
if not by:
by = Person.objects.get(name="(System)")
msg = Message.objects.create(
by = by,
subject = message.get('subject',''),
frm = message.get('from',''),
to = message.get('to',''),
cc = message.get('cc',''),
bcc = message.get('bcc',''),
reply_to = message.get('reply_to',''),
body = get_body(message),
time = utc_from_string(message['date']),
content_type = message.get('content_type', 'text/plain'),
)
return msg
def process_response_email(msg):
"""Saves an incoming message. msg=string. Message "To" field is expected to
be in the format ietf-ipr+[identifier]@ietf.org. Expect to find a message with
a matching value in the reply_to field, associated to an IPR disclosure through
IprEvent. Create a Message object for the incoming message and associate it to
the original message via new IprEvent"""
message = message_from_bytes(force_bytes(msg))
to = message.get('To', '')
# exit if this isn't a response we're interested in (with plus addressing)
local,domain = get_base_ipr_request_address().split('@')
if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to):
return None
try:
to_message = Message.objects.get(reply_to=to)
except Message.DoesNotExist:
log('Error finding matching message ({})'.format(to))
return None
try:
disclosure = to_message.msgevents.first().disclosure
except:
log('Error processing message ({})'.format(to))
return None
ietf_message = message_from_message(message)
IprEvent.objects.create(
type_id = 'msgin',
by = Person.objects.get(name="(System)"),
disclosure = disclosure,
message = ietf_message,
in_reply_to = to_message
)
log("Received IPR email from %s" % ietf_message.frm)
return ietf_message