@@ -95,28 +95,34 @@ class node. Any parts of other types are each stored in separate files
9595from __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
10199import email
102100import 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+
103110from 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
105116from roundup .anypy .email_ import decode_header , message_from_bytes , \
106117 message_from_binary_file
107118from 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
114119from 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
118124try :
119- import gpg , gpg .core , gpg .constants , gpg .constants .sigsum
125+ import gpg , gpg .core , gpg .constants , gpg .constants .sigsum # noqa: E401
120126except 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