Mercurial > p > roundup > code
comparison roundup/roundupdb.py @ 6008:2b53c310089f
flake8 cleanup formatting plus
type comparisons replaced with isinstance
removal of unused imports
bare except -> except Exception:
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sat, 28 Dec 2019 14:51:50 -0500 |
| parents | 71c68961d9f4 |
| children | 8497bf3f23a1 |
comparison
equal
deleted
inserted
replaced
| 6007:e27a240430b8 | 6008:2b53c310089f |
|---|---|
| 18 | 18 |
| 19 """Extending hyperdb with types specific to issue-tracking. | 19 """Extending hyperdb with types specific to issue-tracking. |
| 20 """ | 20 """ |
| 21 __docformat__ = 'restructuredtext' | 21 __docformat__ = 'restructuredtext' |
| 22 | 22 |
| 23 import re, os, smtplib, socket, time | 23 import time |
| 24 import base64, mimetypes | 24 import base64, mimetypes |
| 25 import os.path | |
| 26 import logging | 25 import logging |
| 27 from email import encoders | 26 from email import encoders |
| 28 from email.parser import FeedParser | 27 from email.parser import FeedParser |
| 29 from email.utils import formataddr | 28 from email.utils import formataddr |
| 30 from email.header import Header | 29 from email.header import Header |
| 52 # remember the journal uid for the current journaltag so that: | 51 # remember the journal uid for the current journaltag so that: |
| 53 # a. we don't have to look it up every time we need it, and | 52 # a. we don't have to look it up every time we need it, and |
| 54 # b. if the journaltag disappears during a transaction, we don't barf | 53 # b. if the journaltag disappears during a transaction, we don't barf |
| 55 # (eg. the current user edits their username) | 54 # (eg. the current user edits their username) |
| 56 journal_uid = None | 55 journal_uid = None |
| 56 | |
| 57 def getuid(self): | 57 def getuid(self): |
| 58 """Return the id of the "user" node associated with the user | 58 """Return the id of the "user" node associated with the user |
| 59 that owns this connection to the hyperdatabase.""" | 59 that owns this connection to the hyperdatabase.""" |
| 60 if self.journaltag is None: | 60 if self.journaltag is None: |
| 61 return None | 61 return None |
| 130 except ValueError as e: | 130 except ValueError as e: |
| 131 username = props['username'] | 131 username = props['username'] |
| 132 # Try to make error message less cryptic to the user. | 132 # Try to make error message less cryptic to the user. |
| 133 if str(e) == 'node with key "%s" exists' % username: | 133 if str(e) == 'node with key "%s" exists' % username: |
| 134 raise ValueError( | 134 raise ValueError( |
| 135 _("Username '%s' already exists."%username)) | 135 _("Username '%s' already exists." % username)) |
| 136 else: | 136 else: |
| 137 raise | 137 raise |
| 138 | 138 |
| 139 # clear the props from the otk database | 139 # clear the props from the otk database |
| 140 self.getOTKManager().destroy(otk) | 140 self.getOTKManager().destroy(otk) |
| 141 # commit cl.create (and otk changes) | 141 # commit cl.create (and otk changes) |
| 142 self.commit() | 142 self.commit() |
| 143 | 143 |
| 144 return userid | 144 return userid |
| 145 | |
| 146 | 145 |
| 147 def log_debug(self, msg, *args, **kwargs): | 146 def log_debug(self, msg, *args, **kwargs): |
| 148 """Log a message with level DEBUG.""" | 147 """Log a message with level DEBUG.""" |
| 149 | 148 |
| 150 logger = self.get_logger() | 149 logger = self.get_logger() |
| 170 """ Backends may keep a cache. | 169 """ Backends may keep a cache. |
| 171 It must be cleared at end of commit and rollback methods. | 170 It must be cleared at end of commit and rollback methods. |
| 172 We allow to register user-defined cache-clearing routines | 171 We allow to register user-defined cache-clearing routines |
| 173 that are called by this routine. | 172 that are called by this routine. |
| 174 """ | 173 """ |
| 175 if getattr (self, 'cache_callbacks', None) : | 174 if getattr(self, 'cache_callbacks', None): |
| 176 for method, param in self.cache_callbacks: | 175 for method, param in self.cache_callbacks: |
| 177 method(param) | 176 method(param) |
| 178 | 177 |
| 179 def registerClearCacheCallback(self, method, param = None): | 178 def registerClearCacheCallback(self, method, param=None): |
| 180 """ Register a callback method for clearing the cache. | 179 """ Register a callback method for clearing the cache. |
| 181 It is called with the given param as the only parameter. | 180 It is called with the given param as the only parameter. |
| 182 Even if the parameter is not specified, the method has to | 181 Even if the parameter is not specified, the method has to |
| 183 accept a single parameter. | 182 accept a single parameter. |
| 184 """ | 183 """ |
| 185 if not getattr (self, 'cache_callbacks', None) : | 184 if not getattr(self, 'cache_callbacks', None): |
| 186 self.cache_callbacks = [] | 185 self.cache_callbacks = [] |
| 187 self.cache_callbacks.append ((method, param)) | 186 self.cache_callbacks.append((method, param)) |
| 188 | 187 |
| 189 | 188 |
| 190 class DetectorError(RuntimeError): | 189 class DetectorError(RuntimeError): |
| 191 """ Raised by detectors that want to indicate that something's amiss | 190 """ Raised by detectors that want to indicate that something's amiss |
| 192 """ | 191 """ |
| 193 pass | 192 pass |
| 193 | |
| 194 | 194 |
| 195 # deviation from spec - was called IssueClass | 195 # deviation from spec - was called IssueClass |
| 196 class IssueClass: | 196 class IssueClass: |
| 197 """This class is intended to be mixed-in with a hyperdb backend | 197 """This class is intended to be mixed-in with a hyperdb backend |
| 198 implementation. The backend should provide a mechanism that | 198 implementation. The backend should provide a mechanism that |
| 236 The given text is saved as the body of the message and the node is | 236 The given text is saved as the body of the message and the node is |
| 237 appended to the "messages" field of the specified issue. | 237 appended to the "messages" field of the specified issue. |
| 238 """ | 238 """ |
| 239 | 239 |
| 240 def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy', | 240 def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy', |
| 241 from_address=None, cc=[], bcc=[], cc_emails = [], | 241 from_address=None, cc=[], bcc=[], cc_emails=[], |
| 242 bcc_emails = [], subject=None, | 242 bcc_emails=[], subject=None, |
| 243 note_filter = None, add_headers={}): | 243 note_filter=None, add_headers={}): |
| 244 """Send a message to the members of an issue's nosy list. | 244 """Send a message to the members of an issue's nosy list. |
| 245 | 245 |
| 246 The message is sent only to users on the nosy list who are not | 246 The message is sent only to users on the nosy list who are not |
| 247 already on the "recipients" list for the message. | 247 already on the "recipients" list for the message. |
| 248 | 248 |
| 298 else: | 298 else: |
| 299 # "system message" | 299 # "system message" |
| 300 authid = None | 300 authid = None |
| 301 recipients = [] | 301 recipients = [] |
| 302 | 302 |
| 303 sendto = dict (plain = [], crypt = []) | 303 sendto = dict(plain=[], crypt=[]) |
| 304 bcc_sendto = dict (plain = [], crypt = []) | 304 bcc_sendto = dict(plain=[], crypt=[]) |
| 305 seen_message = {} | 305 seen_message = {} |
| 306 for recipient in recipients: | 306 for recipient in recipients: |
| 307 seen_message[recipient] = 1 | 307 seen_message[recipient] = 1 |
| 308 | 308 |
| 309 def add_recipient(userid, to): | 309 def add_recipient(userid, to): |
| 338 # anonymous | 338 # anonymous |
| 339 if (good_recipient(authid) and | 339 if (good_recipient(authid) and |
| 340 (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or | 340 (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or |
| 341 (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues) or | 341 (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues) or |
| 342 (self.db.config.MESSAGES_TO_AUTHOR == 'nosy' and authid in | 342 (self.db.config.MESSAGES_TO_AUTHOR == 'nosy' and authid in |
| 343 self.get(issueid, whichnosy)))): | 343 self.get(issueid, whichnosy)))): |
| 344 add_recipient(authid, sendto) | 344 add_recipient(authid, sendto) |
| 345 | 345 |
| 346 if authid: | 346 if authid: |
| 347 seen_message[authid] = 1 | 347 seen_message[authid] = 1 |
| 348 | 348 |
| 350 for userid in cc + self.get(issueid, whichnosy): | 350 for userid in cc + self.get(issueid, whichnosy): |
| 351 if good_recipient(userid): | 351 if good_recipient(userid): |
| 352 add_recipient(userid, sendto) | 352 add_recipient(userid, sendto) |
| 353 seen_message[userid] = 1 | 353 seen_message[userid] = 1 |
| 354 if encrypt and not pgproles: | 354 if encrypt and not pgproles: |
| 355 sendto['crypt'].extend (cc_emails) | 355 sendto['crypt'].extend(cc_emails) |
| 356 else: | 356 else: |
| 357 sendto['plain'].extend (cc_emails) | 357 sendto['plain'].extend(cc_emails) |
| 358 | 358 |
| 359 # now deal with bcc people. | 359 # now deal with bcc people. |
| 360 for userid in bcc: | 360 for userid in bcc: |
| 361 if good_recipient(userid): | 361 if good_recipient(userid): |
| 362 add_recipient(userid, bcc_sendto) | 362 add_recipient(userid, bcc_sendto) |
| 363 seen_message[userid] = 1 | 363 seen_message[userid] = 1 |
| 364 if encrypt and not pgproles: | 364 if encrypt and not pgproles: |
| 365 bcc_sendto['crypt'].extend (bcc_emails) | 365 bcc_sendto['crypt'].extend(bcc_emails) |
| 366 else: | 366 else: |
| 367 bcc_sendto['plain'].extend (bcc_emails) | 367 bcc_sendto['plain'].extend(bcc_emails) |
| 368 | 368 |
| 369 if oldvalues: | 369 if oldvalues: |
| 370 note = self.generateChangeNote(issueid, oldvalues) | 370 note = self.generateChangeNote(issueid, oldvalues) |
| 371 else: | 371 else: |
| 372 note = self.generateCreateNote(issueid) | 372 note = self.generateCreateNote(issueid) |
| 385 self.send_message(issueid, msgid, note, sendto['plain'], | 385 self.send_message(issueid, msgid, note, sendto['plain'], |
| 386 from_address, bcc_sendto['plain'], | 386 from_address, bcc_sendto['plain'], |
| 387 subject, add_headers=add_headers) | 387 subject, add_headers=add_headers) |
| 388 if sendto['crypt'] or bcc_sendto['crypt']: | 388 if sendto['crypt'] or bcc_sendto['crypt']: |
| 389 self.send_message(issueid, msgid, note, sendto['crypt'], | 389 self.send_message(issueid, msgid, note, sendto['crypt'], |
| 390 from_address, bcc_sendto['crypt'], subject, crypt=True, | 390 from_address, bcc_sendto['crypt'], subject, |
| 391 add_headers=add_headers) | 391 crypt=True, add_headers=add_headers) |
| 392 | 392 |
| 393 # backwards compatibility - don't remove | 393 # backwards compatibility - don't remove |
| 394 sendmessage = nosymessage | 394 sendmessage = nosymessage |
| 395 | 395 |
| 396 def encrypt_to(self, message, sendto): | 396 def encrypt_to(self, message, sendto): |
| 407 # only first key per email | 407 # only first key per email |
| 408 k = ctx.op_keylist_next() | 408 k = ctx.op_keylist_next() |
| 409 if k is not None: | 409 if k is not None: |
| 410 keys.append(k) | 410 keys.append(k) |
| 411 else: | 411 else: |
| 412 msg = _('No key for "%(adr)s" in keyring')%locals() | 412 msg = _('No key for "%(adr)s" in keyring') % locals() |
| 413 raise MessageSendError(msg) | 413 raise MessageSendError(msg) |
| 414 ctx.op_keylist_end() | 414 ctx.op_keylist_end() |
| 415 ctx.op_encrypt(keys, 1, plain, cipher) | 415 ctx.op_encrypt(keys, 1, plain, cipher) |
| 416 cipher.seek(0,0) | 416 cipher.seek(0, 0) |
| 417 msg = MIMEMultipart('encrypted', boundary=None, _subparts=None, | 417 msg = MIMEMultipart('encrypted', boundary=None, _subparts=None, |
| 418 protocol="application/pgp-encrypted") | 418 protocol="application/pgp-encrypted") |
| 419 part = MIMEBase('application', 'pgp-encrypted') | 419 part = MIMEBase('application', 'pgp-encrypted') |
| 420 part.set_payload("Version: 1\r\n") | 420 part.set_payload("Version: 1\r\n") |
| 421 msg.attach(part) | 421 msg.attach(part) |
| 422 part = MIMEBase('application', 'octet-stream') | 422 part = MIMEBase('application', 'octet-stream') |
| 423 part.set_payload(cipher.read()) | 423 part.set_payload(cipher.read()) |
| 424 msg.attach(part) | 424 msg.attach(part) |
| 425 return msg | 425 return msg |
| 426 | 426 |
| 427 def send_message(self, issueid, msgid, note, sendto, from_address=None, | 427 def send_message(self, issueid, msgid, note, sendto, from_address=None, |
| 428 bcc_sendto=[], subject=None, crypt=False, add_headers={}): | 428 bcc_sendto=[], subject=None, crypt=False, |
| 429 add_headers={}): | |
| 429 '''Actually send the nominated message from this issue to the sendto | 430 '''Actually send the nominated message from this issue to the sendto |
| 430 recipients, with the note appended. It's possible to add | 431 recipients, with the note appended. It's possible to add |
| 431 headers to the message with the add_headers variable. | 432 headers to the message with the add_headers variable. |
| 432 ''' | 433 ''' |
| 433 users = self.db.user | 434 users = self.db.user |
| 443 | 444 |
| 444 # make up a messageid if there isn't one (web edit) | 445 # make up a messageid if there isn't one (web edit) |
| 445 if not messageid: | 446 if not messageid: |
| 446 # this is an old message that didn't get a messageid, so | 447 # this is an old message that didn't get a messageid, so |
| 447 # create one | 448 # create one |
| 448 messageid = "<%s.%s.%s%s@%s>"%(time.time(), | 449 messageid = "<%s.%s.%s%s@%s>" % (time.time(), |
| 449 b2s(base64.b32encode(random_.token_bytes(10))), | 450 b2s(base64.b32encode(random_.token_bytes(10))), |
| 450 self.classname, issueid, self.db.config['MAIL_DOMAIN']) | 451 self.classname, issueid, self.db.config['MAIL_DOMAIN']) |
| 451 if msgid is not None: | 452 if msgid is not None: |
| 452 messages.set(msgid, messageid=messageid) | 453 messages.set(msgid, messageid=messageid) |
| 453 | 454 |
| 454 # compose title | 455 # compose title |
| 455 cn = self.classname | 456 cn = self.classname |
| 456 title = self.get(issueid, 'title') or '%s message copy'%cn | 457 title = self.get(issueid, 'title') or '%s message copy' % cn |
| 457 | 458 |
| 458 # figure author information | 459 # figure author information |
| 459 if msgid: | 460 if msgid: |
| 460 authid = messages.get(msgid, 'author') | 461 authid = messages.get(msgid, 'author') |
| 461 else: | 462 else: |
| 464 if not authname: | 465 if not authname: |
| 465 authname = users.get(authid, 'username', '') | 466 authname = users.get(authid, 'username', '') |
| 466 authaddr = users.get(authid, 'address', '') | 467 authaddr = users.get(authid, 'address', '') |
| 467 | 468 |
| 468 if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: | 469 if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: |
| 469 authaddr = " <%s>" % formataddr( ('',authaddr) ) | 470 authaddr = " <%s>" % formataddr(('', authaddr)) |
| 470 elif authaddr: | 471 elif authaddr: |
| 471 authaddr = "" | 472 authaddr = "" |
| 472 | 473 |
| 473 # make the message body | 474 # make the message body |
| 474 m = [''] | 475 m = [''] |
| 479 | 480 |
| 480 # add author information | 481 # add author information |
| 481 if authid and self.db.config.MAIL_ADD_AUTHORINFO: | 482 if authid and self.db.config.MAIL_ADD_AUTHORINFO: |
| 482 if msgid and len(self.get(issueid, 'messages')) == 1: | 483 if msgid and len(self.get(issueid, 'messages')) == 1: |
| 483 m.append(_("New submission from %(authname)s%(authaddr)s:") | 484 m.append(_("New submission from %(authname)s%(authaddr)s:") |
| 484 % locals()) | 485 % locals()) |
| 485 elif msgid: | 486 elif msgid: |
| 486 m.append(_("%(authname)s%(authaddr)s added the comment:") | 487 m.append(_("%(authname)s%(authaddr)s added the comment:") |
| 487 % locals()) | 488 % locals()) |
| 488 else: | 489 else: |
| 489 m.append(_("Change by %(authname)s%(authaddr)s:") % locals()) | 490 m.append(_("Change by %(authname)s%(authaddr)s:") % locals()) |
| 490 m.append('') | 491 m.append('') |
| 491 | 492 |
| 492 # add the content | 493 # add the content |
| 493 if msgid is not None: | 494 if msgid is not None: |
| 494 m.append(messages.get(msgid, 'content', '')) | 495 m.append(messages.get(msgid, 'content', '')) |
| 495 | 496 |
| 496 # get the files for this message | 497 # get the files for this message |
| 497 message_files = [] | 498 message_files = [] |
| 498 if msgid : | 499 if msgid: |
| 499 for fileid in messages.get(msgid, 'files') : | 500 for fileid in messages.get(msgid, 'files'): |
| 500 # check the attachment size | 501 # check the attachment size |
| 501 filesize = self.db.filesize('file', fileid, None) | 502 filesize = self.db.filesize('file', fileid, None) |
| 502 if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE: | 503 if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE: |
| 503 message_files.append(fileid) | 504 message_files.append(fileid) |
| 504 else: | 505 else: |
| 505 base = self.db.config.TRACKER_WEB | 506 base = self.db.config.TRACKER_WEB |
| 506 link = "".join((base, files.classname, fileid)) | 507 link = "".join((base, files.classname, fileid)) |
| 507 filename = files.get(fileid, 'name') | 508 filename = files.get(fileid, 'name') |
| 508 m.append(_("File '%(filename)s' not attached - " | 509 m.append(_("File '%(filename)s' not attached - " |
| 509 "you can download it from %(link)s.") % locals()) | 510 "you can download it from %(link)s.") % |
| 511 locals()) | |
| 510 | 512 |
| 511 # add the change note | 513 # add the change note |
| 512 if note: | 514 if note: |
| 513 m.append(note) | 515 m.append(note) |
| 514 | 516 |
| 533 from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '') | 535 from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '') |
| 534 if from_tag: | 536 if from_tag: |
| 535 from_tag = ' ' + from_tag | 537 from_tag = ' ' + from_tag |
| 536 | 538 |
| 537 if subject is None: | 539 if subject is None: |
| 538 subject = '[%s%s] %s'%(cn, issueid, title) | 540 subject = '[%s%s] %s' % (cn, issueid, title) |
| 539 | 541 |
| 540 author = (authname + from_tag, from_address) | 542 author = (authname + from_tag, from_address) |
| 541 | 543 |
| 542 # send an individual message per recipient? | 544 # send an individual message per recipient? |
| 543 if self.db.config.NOSY_EMAIL_SENDING != 'single': | 545 if self.db.config.NOSY_EMAIL_SENDING != 'single': |
| 546 sendto = [sendto] | 548 sendto = [sendto] |
| 547 | 549 |
| 548 # tracker sender info | 550 # tracker sender info |
| 549 tracker_name = s2u(self.db.config.TRACKER_NAME) | 551 tracker_name = s2u(self.db.config.TRACKER_NAME) |
| 550 tracker_name = nice_sender_header(tracker_name, from_address, | 552 tracker_name = nice_sender_header(tracker_name, from_address, |
| 551 charset) | 553 charset) |
| 552 | 554 |
| 553 # now send one or more messages | 555 # now send one or more messages |
| 554 # TODO: I believe we have to create a new message each time as we | 556 # TODO: I believe we have to create a new message each time as we |
| 555 # can't fiddle the recipients in the message ... worth testing | 557 # can't fiddle the recipients in the message ... worth testing |
| 556 # and/or fixing some day | 558 # and/or fixing some day |
| 559 # create the message | 561 # create the message |
| 560 mailer = Mailer(self.db.config) | 562 mailer = Mailer(self.db.config) |
| 561 | 563 |
| 562 message = mailer.get_standard_message(multipart=message_files) | 564 message = mailer.get_standard_message(multipart=message_files) |
| 563 | 565 |
| 564 # set reply-to as requested by config option TRACKER_REPLYTO_ADDRESS | 566 # set reply-to as requested by config option |
| 567 # TRACKER_REPLYTO_ADDRESS | |
| 565 replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS | 568 replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS |
| 566 if replyto_config: | 569 if replyto_config: |
| 567 if replyto_config == "AUTHOR": | 570 if replyto_config == "AUTHOR": |
| 568 # note that authaddr at this point is already surrounded by < >, so | 571 # note that authaddr at this point is already |
| 569 # get the original address from the db as nice_send_header adds < > | 572 # surrounded by < >, so get the original address |
| 570 replyto_addr = nice_sender_header(authname, users.get(authid, 'address', ''), charset) | 573 # from the db as nice_send_header adds < > |
| 574 replyto_addr = nice_sender_header(authname, | |
| 575 users.get(authid, 'address', ''), charset) | |
| 571 else: | 576 else: |
| 572 replyto_addr = replyto_config | 577 replyto_addr = replyto_config |
| 573 else: | 578 else: |
| 574 replyto_addr = tracker_name | 579 replyto_addr = tracker_name |
| 575 message['Reply-To'] = replyto_addr | 580 message['Reply-To'] = replyto_addr |
| 608 values = self.get(issueid, propname) | 613 values = self.get(issueid, propname) |
| 609 if not values: | 614 if not values: |
| 610 continue | 615 continue |
| 611 values = [cl.get(v, label) for v in values] | 616 values = [cl.get(v, label) for v in values] |
| 612 values = ', '.join(values) | 617 values = ', '.join(values) |
| 613 header = "X-Roundup-%s-%s"%(self.classname, propname) | 618 header = "X-Roundup-%s-%s" % (self.classname, propname) |
| 614 try: | 619 try: |
| 615 values.encode('ascii') | 620 values.encode('ascii') |
| 616 message[header] = values | 621 message[header] = values |
| 617 except UnicodeError: | 622 except UnicodeError: |
| 618 message[header] = Header(values, charset) | 623 message[header] = Header(values, charset) |
| 619 | 624 |
| 620 # Add header for main id number to make filtering | 625 # Add header for main id number to make filtering |
| 621 # email easier than extracting from subject line. | 626 # email easier than extracting from subject line. |
| 622 header = "X-Roundup-%s-Id"%(self.classname) | 627 header = "X-Roundup-%s-Id" % (self.classname) |
| 623 values = issueid | 628 values = issueid |
| 624 try: | 629 try: |
| 625 values.encode('ascii') | 630 values.encode('ascii') |
| 626 message[header] = values | 631 message[header] = values |
| 627 except UnicodeError: | 632 except UnicodeError: |
| 685 main, sub = mime_type.split('/') | 690 main, sub = mime_type.split('/') |
| 686 part = MIMEBase(main, sub) | 691 part = MIMEBase(main, sub) |
| 687 part.set_payload(content) | 692 part.set_payload(content) |
| 688 encoders.encode_base64(part) | 693 encoders.encode_base64(part) |
| 689 cd = 'Content-Disposition' | 694 cd = 'Content-Disposition' |
| 690 part[cd] = 'attachment;\n filename="%s"'%name | 695 part[cd] = 'attachment;\n filename="%s"' % name |
| 691 message.attach(part) | 696 message.attach(part) |
| 692 | 697 |
| 693 else: | 698 else: |
| 694 message.set_payload(body, message.get_charset()) | 699 message.set_payload(body, message.get_charset()) |
| 695 | 700 |
| 696 if crypt: | 701 if crypt: |
| 697 send_msg = self.encrypt_to (message, sendto) | 702 send_msg = self.encrypt_to(message, sendto) |
| 698 else: | 703 else: |
| 699 send_msg = message | 704 send_msg = message |
| 700 mailer.set_message_attributes(send_msg, sendto, subject, author) | 705 mailer.set_message_attributes(send_msg, sendto, subject, author) |
| 701 if crypt: | 706 if crypt: |
| 702 send_msg ['Message-Id'] = message ['Message-Id'] | 707 send_msg['Message-Id'] = message['Message-Id'] |
| 703 send_msg ['Reply-To'] = message ['Reply-To'] | 708 send_msg['Reply-To'] = message['Reply-To'] |
| 704 if message.get ('In-Reply-To'): | 709 if message.get('In-Reply-To'): |
| 705 send_msg ['In-Reply-To'] = message ['In-Reply-To'] | 710 send_msg['In-Reply-To'] = message['In-Reply-To'] |
| 706 | 711 |
| 707 if sendto: | 712 if sendto: |
| 708 mailer.smtp_send(sendto, send_msg.as_string()) | 713 mailer.smtp_send(sendto, send_msg.as_string()) |
| 709 if first: | 714 if first: |
| 710 if crypt: | 715 if crypt: |
| 711 # send individual bcc mails, otherwise receivers can | 716 # send individual bcc mails, otherwise receivers can |
| 712 # deduce bcc recipients from keys in message | 717 # deduce bcc recipients from keys in message |
| 713 for bcc in bcc_sendto: | 718 for bcc in bcc_sendto: |
| 714 send_msg = self.encrypt_to (message, [bcc]) | 719 send_msg = self.encrypt_to(message, [bcc]) |
| 715 send_msg ['Message-Id'] = message ['Message-Id'] | 720 send_msg['Message-Id'] = message['Message-Id'] |
| 716 send_msg ['Reply-To'] = message ['Reply-To'] | 721 send_msg['Reply-To'] = message['Reply-To'] |
| 717 if message.get ('In-Reply-To'): | 722 if message.get('In-Reply-To'): |
| 718 send_msg ['In-Reply-To'] = message ['In-Reply-To'] | 723 send_msg['In-Reply-To'] = message['In-Reply-To'] |
| 719 mailer.smtp_send([bcc], send_msg.as_string()) | 724 mailer.smtp_send([bcc], send_msg.as_string()) |
| 720 elif bcc_sendto: | 725 elif bcc_sendto: |
| 721 mailer.smtp_send(bcc_sendto, send_msg.as_string()) | 726 mailer.smtp_send(bcc_sendto, send_msg.as_string()) |
| 722 first = False | 727 first = False |
| 723 | 728 |
| 725 ''' Add a signature to the e-mail with some useful information | 730 ''' Add a signature to the e-mail with some useful information |
| 726 ''' | 731 ''' |
| 727 # simplistic check to see if the url is valid, | 732 # simplistic check to see if the url is valid, |
| 728 # then append a trailing slash if it is missing | 733 # then append a trailing slash if it is missing |
| 729 base = self.db.config.TRACKER_WEB | 734 base = self.db.config.TRACKER_WEB |
| 730 if (not isinstance(base , type('')) or | 735 if (not isinstance(base, type('')) or |
| 731 not (base.startswith('http://') or base.startswith('https://'))): | 736 not (base.startswith('http://') or base.startswith('https://'))): |
| 732 web = "Configuration Error: TRACKER_WEB isn't a " \ | 737 web = "Configuration Error: TRACKER_WEB isn't a " \ |
| 733 "fully-qualified URL" | 738 "fully-qualified URL" |
| 734 else: | 739 else: |
| 735 if not base.endswith('/'): | 740 if not base.endswith('/'): |
| 736 base = base + '/' | 741 base = base + '/' |
| 737 web = base + self.classname + issueid | 742 web = base + self.classname + issueid |
| 738 | 743 |
| 739 # ensure the email address is properly quoted | 744 # ensure the email address is properly quoted |
| 740 email = formataddr((self.db.config.TRACKER_NAME, | 745 email = formataddr((self.db.config.TRACKER_NAME, |
| 741 self.db.config.TRACKER_EMAIL)) | 746 self.db.config.TRACKER_EMAIL)) |
| 742 | 747 |
| 743 line = '_' * max(len(web)+2, len(email)) | 748 line = '_' * max(len(web)+2, len(email)) |
| 744 return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line) | 749 return '\n%s\n%s\n<%s>\n%s' % (line, email, web, line) |
| 745 | |
| 746 | 750 |
| 747 def generateCreateNote(self, issueid): | 751 def generateCreateNote(self, issueid): |
| 748 """Generate a create note that lists initial property values | 752 """Generate a create note that lists initial property values |
| 749 """ | 753 """ |
| 750 cn = self.classname | 754 cn = self.classname |
| 781 value = ', '.join(value) | 785 value = ', '.join(value) |
| 782 else: | 786 else: |
| 783 value = str(value) | 787 value = str(value) |
| 784 if '\n' in value: | 788 if '\n' in value: |
| 785 value = '\n'+self.indentChangeNoteValue(value) | 789 value = '\n'+self.indentChangeNoteValue(value) |
| 786 m.append('%s: %s'%(propname, value)) | 790 m.append('%s: %s' % (propname, value)) |
| 787 m.insert(0, '----------') | 791 m.insert(0, '----------') |
| 788 m.insert(0, '') | 792 m.insert(0, '') |
| 789 return '\n'.join(m) | 793 return '\n'.join(m) |
| 790 | 794 |
| 791 def generateChangeNote(self, issueid, oldvalues): | 795 def generateChangeNote(self, issueid, oldvalues): |
| 792 """Generate a change note that lists property changes | 796 """Generate a change note that lists property changes |
| 793 """ | 797 """ |
| 794 if not isinstance(oldvalues, type({})): | 798 if not isinstance(oldvalues, type({})): |
| 795 raise TypeError("'oldvalues' must be dict-like, not %s."% | 799 raise TypeError("'oldvalues' must be dict-like, not %s." % |
| 796 type(oldvalues)) | 800 type(oldvalues)) |
| 797 | 801 |
| 798 cn = self.classname | 802 cn = self.classname |
| 799 cl = self.db.classes[cn] | 803 cl = self.db.classes[cn] |
| 800 changed = {} | 804 changed = {} |
| 801 props = cl.getprops(protected=0) | 805 props = cl.getprops(protected=0) |
| 802 | 806 |
| 803 # determine what changed | 807 # determine what changed |
| 804 for key in oldvalues.keys(): | 808 for key in oldvalues.keys(): |
| 805 if key in ['files','messages']: | 809 if key in ['files', 'messages']: |
| 806 continue | 810 continue |
| 807 if key in ('actor', 'activity', 'creator', 'creation'): | 811 if key in ('actor', 'activity', 'creator', 'creation'): |
| 808 continue | 812 continue |
| 809 # not all keys from oldvalues might be available in database | 813 # not all keys from oldvalues might be available in database |
| 810 # this happens when property was deleted | 814 # this happens when property was deleted |
| 814 continue | 818 continue |
| 815 # the old value might be non existent | 819 # the old value might be non existent |
| 816 # this happens when property was added | 820 # this happens when property was added |
| 817 try: | 821 try: |
| 818 old_value = oldvalues[key] | 822 old_value = oldvalues[key] |
| 819 if type(new_value) is type([]): | 823 if isinstance(new_value, type([])): |
| 820 new_value.sort() | 824 new_value.sort() |
| 821 old_value.sort() | 825 old_value.sort() |
| 822 if new_value != old_value: | 826 if new_value != old_value: |
| 823 changed[key] = old_value | 827 changed[key] = old_value |
| 824 except: | 828 except Exception: |
| 825 changed[key] = new_value | 829 changed[key] = new_value |
| 826 | 830 |
| 827 # list the changes | 831 # list the changes |
| 828 m = [] | 832 m = [] |
| 829 changed_items = sorted(changed.items()) | 833 changed_items = sorted(changed.items()) |
| 843 value = '' | 847 value = '' |
| 844 if oldvalue: | 848 if oldvalue: |
| 845 oldvalue = link.get(oldvalue, key) | 849 oldvalue = link.get(oldvalue, key) |
| 846 else: | 850 else: |
| 847 oldvalue = '' | 851 oldvalue = '' |
| 848 change = '%s -> %s'%(oldvalue, value) | 852 change = '%s -> %s' % (oldvalue, value) |
| 849 elif isinstance(prop, hyperdb.Multilink): | 853 elif isinstance(prop, hyperdb.Multilink): |
| 850 change = '' | 854 change = '' |
| 851 if value is None: value = [] | 855 if value is None: value = [] |
| 852 if oldvalue is None: oldvalue = [] | 856 if oldvalue is None: oldvalue = [] |
| 853 l = [] | 857 l = [] |
| 860 l.append(link.get(entry, key)) | 864 l.append(link.get(entry, key)) |
| 861 else: | 865 else: |
| 862 l.append(entry) | 866 l.append(entry) |
| 863 if l: | 867 if l: |
| 864 l.sort() | 868 l.sort() |
| 865 change = '+%s'%(', '.join(l)) | 869 change = '+%s' % (', '.join(l)) |
| 866 l = [] | 870 l = [] |
| 867 # check for removals | 871 # check for removals |
| 868 for entry in oldvalue: | 872 for entry in oldvalue: |
| 869 if entry in value: continue | 873 if entry in value: continue |
| 870 if key: | 874 if key: |
| 871 l.append(link.get(entry, key)) | 875 l.append(link.get(entry, key)) |
| 872 else: | 876 else: |
| 873 l.append(entry) | 877 l.append(entry) |
| 874 if l: | 878 if l: |
| 875 l.sort() | 879 l.sort() |
| 876 change += ' -%s'%(', '.join(l)) | 880 change += ' -%s' % (', '.join(l)) |
| 877 else: | 881 else: |
| 878 change = '%s -> %s'%(oldvalue, value) | 882 change = '%s -> %s' % (oldvalue, value) |
| 879 if '\n' in change: | 883 if '\n' in change: |
| 880 value = self.indentChangeNoteValue(str(value)) | 884 value = self.indentChangeNoteValue(str(value)) |
| 881 oldvalue = self.indentChangeNoteValue(str(oldvalue)) | 885 oldvalue = self.indentChangeNoteValue(str(oldvalue)) |
| 882 change = _('\nNow:\n%(new)s\nWas:\n%(old)s') % { | 886 change = _('\nNow:\n%(new)s\nWas:\n%(old)s') % { |
| 883 "new": value, "old": oldvalue} | 887 "new": value, "old": oldvalue} |
| 884 m.append('%s: %s'%(propname, change)) | 888 m.append('%s: %s' % (propname, change)) |
| 885 if m: | 889 if m: |
| 886 m.insert(0, '----------') | 890 m.insert(0, '----------') |
| 887 m.insert(0, '') | 891 m.insert(0, '') |
| 888 return '\n'.join(m) | 892 return '\n'.join(m) |
| 889 | 893 |
| 890 def indentChangeNoteValue(self, text): | 894 def indentChangeNoteValue(self, text): |
| 891 lines = text.rstrip('\n').split('\n') | 895 lines = text.rstrip('\n').split('\n') |
| 892 lines = [ ' '+line for line in lines ] | 896 lines = [' '+line for line in lines] |
| 893 return '\n'.join(lines) | 897 return '\n'.join(lines) |
| 894 | 898 |
| 895 # vim: set filetype=python sts=4 sw=4 et si : | 899 # vim: set filetype=python sts=4 sw=4 et si : |
