Skip to content

Commit 1f04997

Browse files
committed
flake8 changes.
Also identified a possible crash when a message arrives without an issue designator. https://issues.roundup-tracker.org/issue2551232.
1 parent a7de476 commit 1f04997

File tree

1 file changed

+102
-60
lines changed

1 file changed

+102
-60
lines changed

roundup/mailgw.py

Lines changed: 102 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -95,28 +95,34 @@ class node. Any parts of other types are each stored in separate files
9595
from __future__ import print_function
9696
__docformat__ = 'restructuredtext'
9797

98-
import base64, re, os, io, functools
99-
import time, sys, logging
100-
import traceback
98+
import base64
10199
import email
102100
import email.utils
101+
import functools
102+
import io
103+
import logging
104+
import os
105+
import re
106+
import sys
107+
import time
108+
import traceback
109+
103110
from email.generator import Generator
104111

112+
import roundup.anypy.random_ as random_
113+
import roundup.anypy.ssl_ as ssl_
114+
115+
from roundup import configuration, date, exceptions, hyperdb, i18n, password
105116
from roundup.anypy.email_ import decode_header, message_from_bytes, \
106117
message_from_binary_file
107118
from roundup.anypy.my_input import my_input
108-
109-
from roundup import configuration, hyperdb, date, password, exceptions
110-
from roundup.mailer import Mailer
111-
from roundup.i18n import _
112-
from roundup import i18n
113-
from roundup.hyperdb import iter_roles
114119
from roundup.anypy.strings import StringIO, b2s, u2s
115-
import roundup.anypy.random_ as random_
116-
import roundup.anypy.ssl_ as ssl_
120+
from roundup.hyperdb import iter_roles
121+
from roundup.i18n import _
122+
from roundup.mailer import Mailer
117123

118124
try:
119-
import gpg, gpg.core, gpg.constants, gpg.constants.sigsum
125+
import gpg, gpg.core, gpg.constants, gpg.constants.sigsum # noqa: E401
120126
except ImportError:
121127
gpg = None
122128

@@ -352,8 +358,8 @@ def extract_content(self, parent_type=None, ignore_alternatives=False,
352358
if html_part:
353359
# attachment should be added elsewhere.
354360
pass
355-
elif content_found or content_type != \
356-
'multipart/alternative':
361+
elif (content_found or
362+
content_type != 'multipart/alternative'):
357363
attachments.append(part.text_as_attachment())
358364
elif html_part_found:
359365
# text/plain part found after html
@@ -409,7 +415,7 @@ def get_filename(self):
409415
# because it seems that email.message.Message isn't a new-style
410416
# class in python2
411417
fn = email.message.Message.get_filename(self)
412-
if not fn :
418+
if not fn:
413419
return fn
414420
h = []
415421
for x, t in decode_header(fn):
@@ -563,10 +569,11 @@ def handle_ignore(self):
563569
'''
564570
if self.message.get_header('x-roundup-loop', ''):
565571
raise IgnoreLoop
566-
if (self.message.get_header('precedence', '') == 'bulk'
567-
or self.message.get_header('auto-submitted', 'no').rstrip().lower() \
568-
!= 'no'
569-
or self.subject.lower().find("autoreply") > 0):
572+
if (
573+
self.message.get_header('precedence', '') == 'bulk' or
574+
self.message.get_header('auto-submitted',
575+
'no').rstrip().lower() != 'no' or
576+
self.subject.lower().find("autoreply") > 0):
570577
raise IgnoreBulk
571578

572579
def handle_help(self):
@@ -596,9 +603,9 @@ def parse_subject(self):
596603

597604
sd_open, sd_close = self.config['MAILGW_SUBJECT_SUFFIX_DELIMITERS']
598605
delim_open = re.escape(sd_open)
599-
if delim_open in '[(': delim_open = '\\' + delim_open
606+
if delim_open in '[(': delim_open = '\\' + delim_open # noqa: E701
600607
delim_close = re.escape(sd_close)
601-
if delim_close in '[(': delim_close = '\\' + delim_close
608+
if delim_close in '[(': delim_close = '\\' + delim_close # noqa: E701
602609

603610
# Look for Re: et. al. Used later on for MAILGW_SUBJECT_CONTENT_MATCH
604611
re_re = r"(?P<refwd>%s)\s*" % self.config["MAILGW_REFWD_RE"].pattern
@@ -644,8 +651,8 @@ def parse_subject(self):
644651
q = ''
645652
if self.matches['quote']:
646653
q = '"?'
647-
args_re = r'(?P<argswhole>%s(?P<args>[^%s]*)%s)%s$' % (delim_open,
648-
delim_close, delim_close, q)
654+
args_re = r'(?P<argswhole>%s(?P<args>[^%s]*)%s)%s$' % (
655+
delim_open, delim_close, delim_close, q)
649656
m = re.search(args_re, tmpsubject.strip(), re.IGNORECASE | re.VERBOSE)
650657
if m:
651658
self.matches.update(m.groupdict())
@@ -766,11 +773,28 @@ def get_nodeid(self):
766773
nodeid = self.matches['nodeid']
767774

768775
# try in-reply-to to match the message if there's no nodeid
776+
# FIXME: possible crash if message linked to multiple issues
777+
# Use the in-reply-to of the current message to find an id
778+
# for the message being replied to.
779+
# Then search the current class (probably issue) for an issue
780+
# that has the parent_message id in the issue's messages
781+
# property. Then use this id as the node to update. HOWEVER if
782+
# the reply to message is linked to multiple issues, I think
783+
# this blows up.
784+
# Linking a message to multiple issues can be used to group
785+
# issues so that an update on a child issue is also reflected
786+
# on a parent issue. As the parent and child may have different
787+
# nosy/watchers.
788+
769789
inreplyto = self.message.get_header('in-reply-to') or ''
770790
if nodeid is None and inreplyto:
771-
l = self.db.getclass('msg').stringFind(messageid=inreplyto)
772-
if l:
773-
nodeid = self.cl.filter(None, {'messages': l})[0]
791+
parent_message = self.db.getclass('msg').stringFind(
792+
messageid=inreplyto)
793+
# FIXME: if a message is linked to multiple issues, can nodeid
794+
# be a list? If so, will this crash??
795+
if parent_message:
796+
nodeid = self.cl.filter(None,
797+
{'messages': parent_message})[0]
774798

775799
# but we do need either a title or a nodeid...
776800
if nodeid is None and not title:
@@ -791,13 +815,13 @@ def get_nodeid(self):
791815
# activity.
792816
tmatch_mode = self.config['MAILGW_SUBJECT_CONTENT_MATCH']
793817
if tmatch_mode != 'never' and nodeid is None and self.matches['refwd']:
794-
l = self.cl.stringFind(title=title)
818+
title_match_ids = self.cl.stringFind(title=title)
795819
limit = None
796820
if (tmatch_mode.startswith('creation') or
797821
tmatch_mode.startswith('activity')):
798822
limit, interval = tmatch_mode.split(' ', 1)
799823
threshold = date.Date('.') - date.Interval(interval)
800-
for id in l:
824+
for id in title_match_ids:
801825
if limit:
802826
if threshold < self.cl.get(id, limit):
803827
nodeid = id
@@ -869,7 +893,8 @@ class of node
869893
'''
870894
if self.nodeid:
871895
if not self.db.security.hasPermission('Edit', self.author,
872-
self.classname, itemid=self.nodeid):
896+
self.classname,
897+
itemid=self.nodeid):
873898
raise Unauthorized(_(
874899
'You are not permitted to edit %(classname)s.'
875900
) % self.__dict__)
@@ -970,8 +995,8 @@ def get_props(self):
970995

971996
# set the issue title to the subject
972997
title = title.strip()
973-
if (title and 'title' in self.properties and 'title'
974-
not in issue_props):
998+
if (title and 'title' in self.properties and
999+
'title' not in issue_props):
9751000
issue_props['title'] = title
9761001
if (self.nodeid and 'title' in self.properties and not
9771002
self.config['MAILGW_SUBJECT_UPDATES_TITLE']):
@@ -995,7 +1020,8 @@ def pgp_role():
9951020
or we will skip PGP processing
9961021
"""
9971022
if self.config.PGP_ROLES:
998-
return self.db.user.has_role(self.author,
1023+
return self.db.user.has_role(
1024+
self.author,
9991025
*iter_roles(self.config.PGP_ROLES))
10001026
else:
10011027
return True
@@ -1039,8 +1065,9 @@ def pgp_role():
10391065
# we get here. Try decrypting it again if we don't
10401066
# need signatures.
10411067
if encr_only:
1042-
message = self.message.decrypt(author_address,
1043-
may_be_unsigned=encr_only)
1068+
message = self.message.decrypt(
1069+
author_address,
1070+
may_be_unsigned=encr_only)
10441071
else:
10451072
# something failed with the message decryption/sig
10461073
# chain. Pass the error up.
@@ -1090,8 +1117,11 @@ def create_files(self):
10901117
else:
10911118
files.append(fileid)
10921119
# allowed to attach the files to an existing node?
1093-
if self.nodeid and not self.db.security.hasPermission('Edit',
1094-
self.author, self.classname, 'files'):
1120+
if self.nodeid and \
1121+
not self.db.security.hasPermission('Edit',
1122+
self.author,
1123+
self.classname,
1124+
'files'):
10951125
raise Unauthorized(_(
10961126
'You are not permitted to add files to %(classname)s.'
10971127
) % self.__dict__)
@@ -1118,7 +1148,8 @@ def create_msg(self):
11181148
messageid = self.message.get_header('message-id')
11191149
# generate a messageid if there isn't one
11201150
if not messageid:
1121-
messageid = "<%s.%s.%s%s@%s>" % (time.time(),
1151+
messageid = "<%s.%s.%s%s@%s>" % (
1152+
time.time(),
11221153
b2s(base64.b32encode(random_.token_bytes(10))),
11231154
self.classname, self.nodeid, self.config['MAIL_DOMAIN'])
11241155

@@ -1139,7 +1170,8 @@ def create_msg(self):
11391170
'You are not permitted to create messages.'))
11401171

11411172
try:
1142-
message_id = self.db.msg.create(author=self.author,
1173+
message_id = self.db.msg.create(
1174+
author=self.author,
11431175
recipients=self.recipients, date=date.Date('.'),
11441176
summary=summary, content=content,
11451177
messageid=messageid, inreplyto=inreplyto, **self.msg_props)
@@ -1149,8 +1181,11 @@ def create_msg(self):
11491181
%(error)s
11501182
""") % locals())
11511183
# allowed to attach the message to the existing node?
1152-
if self.nodeid and not self.db.security.hasPermission('Edit',
1153-
self.author, self.classname, 'messages'):
1184+
if self.nodeid and \
1185+
not self.db.security.hasPermission('Edit',
1186+
self.author,
1187+
self.classname,
1188+
'messages'):
11541189
raise Unauthorized(_(
11551190
'You are not permitted to add messages to %(classname)s.'
11561191
) % self.__dict__)
@@ -1174,21 +1209,25 @@ def create_node(self):
11741209
for prop in self.props.keys():
11751210
if not self.db.security.hasPermission('Edit', self.author,
11761211
classname, prop):
1177-
raise Unauthorized(_('You are not permitted to edit '
1212+
raise Unauthorized(_(
1213+
'You are not permitted to edit '
11781214
'property %(prop)s of class %(classname)s.') %
11791215
locals())
11801216
self.cl.set(self.nodeid, **self.props)
11811217
else:
11821218
# Check permissions for each property
11831219
for prop in self.props.keys():
11841220
if not self.db.security.hasPermission('Create',
1185-
self.author, classname, prop):
1186-
raise Unauthorized(_('You are not permitted to set '
1221+
self.author,
1222+
classname,
1223+
prop):
1224+
raise Unauthorized(_(
1225+
'You are not permitted to set '
11871226
'property %(prop)s of class %(classname)s.') %
11881227
locals())
11891228
self.nodeid = self.cl.create(**self.props)
1190-
except (TypeError, IndexError,
1191-
ValueError, exceptions.Reject) as message: # noqa: F841
1229+
except (TypeError, IndexError, # noqa: F841
1230+
ValueError, exceptions.Reject) as message:
11921231
self.mailgw.logger.exception(
11931232
"Rejecting email due to node creation error:")
11941233
raise MailUsageError(_("""
@@ -1357,7 +1396,7 @@ class mboxRoundupMessage(mailbox.mboxMessage, RoundupMessage):
13571396
def do_imap(self, server, user='', password='', mailbox='', ssl=0, cram=0):
13581397
''' Do an IMAP connection
13591398
'''
1360-
import getpass, imaplib, socket
1399+
import getpass, imaplib, socket # noqa: E401
13611400
try:
13621401
if not user:
13631402
user = my_input('User: ')
@@ -1436,7 +1475,7 @@ def do_pop(self, server, user='', password='', ssl=False):
14361475
def _do_pop(self, server, user, password, apop, ssl):
14371476
'''Read a series of messages from the specified POP server.
14381477
'''
1439-
import getpass, poplib, socket
1478+
import getpass, poplib, socket # noqa: E401
14401479
# Monkey-patch poplib to have a large line-limit
14411480
# Seems that in python2.7 poplib applies a line-length limit not
14421481
# just to the lines that take care of the pop3 protocol but also
@@ -1605,14 +1644,15 @@ def handle_message(self, message):
16051644
*Translate* test cases in test/test_mailgw.py. This method
16061645
can't be tested directly because it opens the instance
16071646
erasing the database mocked by the test harness.
1608-
1647+
16091648
'''
16101649
# get database handle for handling one email
16111650
self.db = self.instance.open('admin')
16121651

16131652
language = self.instance.config["MAILGW_LANGUAGE"] or self.instance.config["TRACKER_LANGUAGE"]
1614-
self.db.i18n = i18n.get_translation(language,
1615-
tracker_home=self.instance.config["TRACKER_HOME"])
1653+
self.db.i18n = i18n.get_translation(
1654+
language,
1655+
tracker_home=self.instance.config["TRACKER_HOME"])
16161656

16171657
global _
16181658
_ = self.db.i18n.gettext
@@ -1697,7 +1737,8 @@ def get_class_arguments(self, class_type, classname=None):
16971737
cls = cls_lookup.get(current_type, current_type)
16981738
temp_cl = self.db.getclass(cls)
16991739
errors, props = setPropArrayFromString(self,
1700-
temp_cl, propstring.strip())
1740+
temp_cl,
1741+
propstring.strip())
17011742

17021743
if errors:
17031744
mailadmin = self.instance.config['ADMIN_EMAIL']
@@ -1808,8 +1849,9 @@ def uidFromAddress(db, address, create=1, **user_props):
18081849

18091850
# create!
18101851
try:
1811-
return db.user.create(username=trying, address=address,
1812-
realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES,
1852+
return db.user.create(
1853+
username=trying, address=address, realname=realname,
1854+
roles=db.config.NEW_EMAIL_USER_ROLES,
18131855
password=password.Password(password.generatePassword(),
18141856
config=db.config),
18151857
**user_props)
@@ -1884,7 +1926,7 @@ def parseContent(content, keep_citations=None, keep_body=None,
18841926

18851927
# extract out the summary from the message
18861928
summary = ''
1887-
l = []
1929+
kept_lines = []
18881930
# find last non-empty section for signature matching
18891931
last_nonempty = len(sections) - 1
18901932
while last_nonempty and not sections[last_nonempty]:
@@ -1901,20 +1943,20 @@ def parseContent(content, keep_citations=None, keep_body=None,
19011943
if ns and not quote_1st and lines[0] and not keep_citations:
19021944
# we drop only first-lines ending in ':' (e.g. 'XXX wrote:')
19031945
if not lines[0].endswith(':'):
1904-
l.append(lines[0])
1946+
kept_lines.append(lines[0])
19051947
# see if there's a response somewhere inside this section (ie.
19061948
# no blank line between quoted message and response)
1907-
for n, line in enumerate(lines[1:]):
1949+
for _n, line in enumerate(lines[1:]):
19081950
if line and line[0] not in '>|':
19091951
break
19101952
else:
19111953
# we keep quoted bits if specified in the config
19121954
if keep_citations:
1913-
l.append(section)
1955+
kept_lines.append(section)
19141956
continue
19151957
# keep this section - it has reponse stuff in it
19161958
if not keep_citations:
1917-
lines = lines[n + 1:]
1959+
lines = lines[_n + 1:]
19181960
section = '\n'.join(lines)
19191961

19201962
is_last = ns == last_nonempty
@@ -1933,7 +1975,7 @@ def parseContent(content, keep_citations=None, keep_body=None,
19331975
break
19341976

19351977
# and add the section to the output
1936-
l.append(section)
1978+
kept_lines.append(section)
19371979

19381980
# figure the summary - find the first sentence-ending punctuation or the
19391981
# first whole line, whichever is longest
@@ -1948,7 +1990,7 @@ def parseContent(content, keep_citations=None, keep_body=None,
19481990
# Now reconstitute the message content minus the bits we don't care
19491991
# about.
19501992
if not keep_body:
1951-
content = '\n\n'.join(l)
1993+
content = '\n\n'.join(kept_lines)
19521994

19531995
return summary, content
19541996

0 commit comments

Comments
 (0)