From: <ke...@us...> - 2006-06-27 11:20:57
|
Revision: 3258 Author: kevca Date: 2006-06-27 04:20:24 -0700 (Tue, 27 Jun 2006) ViewCVS: http://svn.sourceforge.net/mailmanager/?rev=3258&view=rev Log Message: ----------- Tagging 2.1-rc6-pre5 Modified Paths: -------------- MailManager/tags/RELENG_2_1_RC6_PRE5/docs/development/source/sections/ruleset.xml MailManager/tags/RELENG_2_1_RC6_PRE5/docs/development/source/sections/testing.xml MailManager/tags/RELENG_2_1_RC6_PRE5/docs/development/source/sections/unicode.xml Added Paths: ----------- MailManager/tags/RELENG_2_1_RC6_PRE5/ MailManager/tags/RELENG_2_1_RC6_PRE5/Extensions/TicketPluggableBrain.py MailManager/tags/RELENG_2_1_RC6_PRE5/MMImportHandler.py MailManager/tags/RELENG_2_1_RC6_PRE5/MailManager.py MailManager/tags/RELENG_2_1_RC6_PRE5/Makefile MailManager/tags/RELENG_2_1_RC6_PRE5/sql/v2_1/addMessage.zsql MailManager/tags/RELENG_2_1_RC6_PRE5/sql/v2_1/listQueuedTickets.zsql MailManager/tags/RELENG_2_1_RC6_PRE5/support/login.py MailManager/tags/RELENG_2_1_RC6_PRE5/tests/testDatabase.py MailManager/tags/RELENG_2_1_RC6_PRE5/version.txt MailManager/tags/RELENG_2_1_RC6_PRE5/www/SystemSettings.zpt MailManager/tags/RELENG_2_1_RC6_PRE5/www/macros.zpt MailManager/tags/RELENG_2_1_RC6_PRE5/www/ticket_index_html.zpt Removed Paths: ------------- MailManager/tags/RELENG_2_1_RC6_PRE5/Extensions/TicketPluggableBrain.py MailManager/tags/RELENG_2_1_RC6_PRE5/MMImportHandler.py MailManager/tags/RELENG_2_1_RC6_PRE5/MailManager.py MailManager/tags/RELENG_2_1_RC6_PRE5/Makefile MailManager/tags/RELENG_2_1_RC6_PRE5/sql/v2_1/addMessage.zsql MailManager/tags/RELENG_2_1_RC6_PRE5/sql/v2_1/listQueuedTickets.zsql MailManager/tags/RELENG_2_1_RC6_PRE5/support/login.py MailManager/tags/RELENG_2_1_RC6_PRE5/tests/testDatabase.py MailManager/tags/RELENG_2_1_RC6_PRE5/version.txt MailManager/tags/RELENG_2_1_RC6_PRE5/www/SystemSettings.zpt MailManager/tags/RELENG_2_1_RC6_PRE5/www/macros.zpt MailManager/tags/RELENG_2_1_RC6_PRE5/www/ticket_index_html.zpt Copied: MailManager/tags/RELENG_2_1_RC6_PRE5 (from rev 3250, MailManager/branches/RELENG_2_1) Deleted: MailManager/tags/RELENG_2_1_RC6_PRE5/Extensions/TicketPluggableBrain.py =================================================================== --- MailManager/branches/RELENG_2_1/Extensions/TicketPluggableBrain.py 2006-06-26 17:32:03 UTC (rev 3250) +++ MailManager/tags/RELENG_2_1_RC6_PRE5/Extensions/TicketPluggableBrain.py 2006-06-27 11:20:24 UTC (rev 3258) @@ -1,1799 +0,0 @@ -# (c) Copyright Logicalware Ltd 2002-2006 -# Logicalware MailManager comes with ABSOLUTELY NO WARRANTY. Further details -# including conditions of redistribution are contained in LICENSE.txt -# http://www.logicalware.org/ -# $Id$ - -# Zope modules. -from Globals import InitializeClass -from AccessControl import ClassSecurityInfo, getSecurityManager -from DateTime import DateTime -import mx.DateTime -from zExceptions.unauthorized import Unauthorized -from zExceptions import BadRequest - -# Modules from this package -from Products.MailManager.MailMixin import MailMixin - -# 3rd party modules. -try: - from psycopg import Binary -except ImportError: - pass -try: - from stripogram import html2safehtml -except ImportError: - from Products.stripogram import html2safehtml - -from Products.MailManager.support.html2text import html2text - -# Python library modules. -import re -import operator -from email.Utils import formataddr, parseaddr, make_msgid -from smtplib import SMTPRecipientsRefused - -from Products.MailManager import ruleset - -import zLOG - -from Products.MailManager.ruleset.common import historyItem, ticketState, NoTransitionError, SecurityError - -import logging -from Products.MailManager.support.logger import log - -import pprint - -from AccessControl import getSecurityManager - -from Products.MailManager.support.strptime import strptime - - -from_escape = re.compile('^>*From ') -sig_remover = re.compile('^-- $.*\Z', re.DOTALL | re.MULTILINE) - - -class TicketPluggableBrain(MailMixin): - - security = ClassSecurityInfo() - security.setPermissionDefault('MailManager Manage Tickets', - ['Tickets', 'Customer']) - - security.declareProtected('MailManager Manage Tickets', 'checkAuthorized') - def checkAuthorized(self): - user = getSecurityManager().getUser() - - if not 'Tickets' in user.roles: - username = user.getUserName() - addrs = self.sql.getCustomerAddresses(sqv_username=username) - if not self['from_email'] in [x.access_email for x in addrs]: - raise Unauthorized("You are not allowed to view this ticket") - - security.declarePublic('__bobo_traverse__') - def __bobo_traverse__(self, REQUEST=None, name=None): - """ Traversal hook to do namespace mangling for putting http in - from of requests. - - We rename the request for local methods so that we are allowing - mangling - attempting to get addMessageToTicket will return - the method http_addMessageToTicket. This allows us to split - off the interface layer from API calls. - """ - # First of all, try and mangle http_ in front of requests - ret = getattr(self, 'http_%s' % name, None) - if ret: return ret - - # Then try and obtain without mangling - ret = getattr(self, name, None) - if ret: return ret - else: - ret = self[name] - if ret: return ret - return name - - - security.declareProtected('MailManager Manage Tickets', 'index_html') - def index_html(self, REQUEST): - """Provide a view for the simple direct traversal.""" - return self.ticket_index_html(REQUEST) - - security.declareProtected('MailManager Manage Tickets', 'findSupporter') - def findSupporter(self, REQUEST, SESSION): - """Find a supporter for this Ticket. - - This method doesn't really do what you might expect. It sets the - find_supporter session variable to the current ticket id. It then - shows the ticket index so the end user can find another ticket. - When viewing other tickets, they will now have an option to make - the other ticket a supporter of the current ticket. This is mainly - handled in the zpts. - """ - SESSION.set('find_supporter', self.id) - return self.ticket_index_html(REQUEST) - - security.declareProtected('MailManager Manage Tickets', 'change') - def change(self, REQUEST): - """Return a view allowing the details to be changed.""" - REQUEST.set('details', 'y') - return self.ticket_index_html(REQUEST) - - - security.declareProtected('MailManager Manage Tickets', 'http_save') - def http_save(self, subject='', assigned='', state='', priority=0, - category0=None, category1=None, category2=None, support_of=None, - event='', transition=None, REQUEST=None, RESPONSE=None): - """ Save changes to a ticket. - - Check what has been changed, record changes in history and then make - the changes to the ticket. - - transition is the id of transition from the ruleset engine which is - being used to make this change. - - event is the name of the event which is causing the transition to - occur. - - FIXME: RULESETMODS - """ - # Sort out encodings first - subject = unicode(subject, 'utf-8') - assigned = unicode(assigned, 'utf-8') - if category0 is not None: - category0 = unicode(category0, 'utf-8') - if category1 is not None: - category1 = unicode(category1, 'utf-8') - if category2 is not None: - category2 = unicode(category2, 'utf-8') - - if REQUEST is not None: - # Check for errors (if called through the web). - error = None - if state == 'Closed' != self.state and not self._okToClose(): - error = 'All supporting tickets must be Closed.' - REQUEST.set('flag_supporters', True) - if support_of and support_of != self.support_of: - # Check ticket to be supported exists, is not Closed and does - # not already support self. - results = self.ticket(id=support_of) - if not results: - error = 'Ticket %06d does not exist.' % support_of - REQUEST.set('flag_support_of', True) - elif results[0].state == 'Closed': - error = 'Ticket %06d is Closed.' % support_of - REQUEST.set('flag_support_of', True) - elif results[0].support_of == self.id: - error = ('Ticket %06d itself supports this ticket.' % - support_of) - REQUEST.set('flag_support_of', True) - if error is not None: - REQUEST.set('error', error) - REQUEST.set('details', 'y') - return self.ticket_index_html(REQUEST) - - changed_by = getSecurityManager().getUser().getUserName() - if changed_by == 'Anonymous User': changed_by = '' - - self.save( - changed_by = changed_by, - subject = subject, - assigned = assigned, - state = state, - priority = priority, - category0 = category0, - category1 = category1, - category2 = category2, - support_of = support_of, - event = event, - transition = transition - ) - - if RESPONSE is not None: - return RESPONSE.redirect('%s/ticket/%06d' % (self.getBaseURL(), - self.id)) - - security.declareProtected('MailManager Manage Tickets', 'save') - def save(self, changed_by, subject='', assigned='', state='', priority=0, - category0=None, category1=None, category2=None, support_of=None, - event='', transition=None): - """ Save changes to a ticket. - - Check what has been changed, record changes in history and then make - the changes to the ticket. - - transition is the id of transition from the ruleset engine which is - being used to make this change. - - event is the name of the event which is causing the transition to - occur. - - changed_by : - - """ - changes = {} - set_date_Closed = False - clear_date_Closed = False - - # Changes need to be done to the local object as well as set in the - # database. Some changes need special SQL parameters. - - if state and state != self.state: - if state == 'Closed': - # Record the date & time the ticket was Closed. - set_date_Closed = True - elif self.state == 'Closed': - # Clear date_Closed when we re-open a ticket. - clear_date_Closed = True - changes['state'] = self.state - if subject and subject != self.subject: - changes['subject'] = self.subject - self.subject = subject - if assigned and assigned != self.assigned: - changes['assigned'] = self.assigned - self.assigned = assigned - # If the assigned user was changed by someone other than the new - # user and the 'Notify users of new tickets' flag is set then let - # the new user know they have got a ticket. - if assigned != changed_by: - if self.account(email=self.account_id)[0].notify_user: - # Hook in user notification - self.notifyUser(new_id=assigned) - if state and state != self.state: - changes['state'] = self.state - self.state = state - if priority and priority != self.priority: - changes['priority'] = self.priority - self.priority = priority - if category0 and category0 != self.category0: - changes['category0'] = self.category0 - self.category0 = category0 - if category1 and category1 != self.category1: - changes['category1'] = self.category1 - self.category1 = category1 - if category2 and category2 != self.category2: - changes['category2'] = self.category2 - self.category2 = category2 - if support_of is not None and support_of != self.support_of: - changes['support_of'] = self.support_of or 0 - self.support_of = support_of - - if changes: - - # If the transition/event don't actually change anything we - # wouldn't want to record them, so don't set these before the - # conditional test above - - changes['transition'] = transition - changes['event'] = event - - self.sql.addHistory(sqv_ticket_id=self.id, - sqv_subject=changes.get('subject', ''), - sqv_assigned=changes.get('assigned', ''), - sqv_state=changes.get('state', ''), - sqv_priority=changes.get('priority'), - sqv_category0=changes.get('category0', ''), - sqv_category1=changes.get('category1', ''), - sqv_category2=changes.get('category2', ''), - sqv_support_of=changes.get('support_of', ''), - sqv_transition=changes.get('transition', ''), - sqv_event=changes.get('event', ''), - sqv_changed_by=changed_by) - self.sql.editTicket(sqv_id=self.id, - sqv_subject=subject or self.subject, - sqv_assigned=assigned or self.assigned, - sqv_state=state or self.state, - sqv_priority=priority or self.priority, - sqv_category0=category0 or self.category0 or '', - sqv_category1=category1 or self.category1 or '', - sqv_category2=category2 or self.category2 or '', - sqv_set_date_Closed=set_date_Closed, - sqv_clear_date_Closed=clear_date_Closed) - # Icky, but let's update the support_of separately. - # This allows the use of clear_support_of to set None - # values - if support_of == 0: - self.sql.editTicket(sqv_id = self.id, sqv_clear_support_of = True) - elif support_of: - self.sql.editTicket(sqv_id = self.id, sqv_set_support_of = True, sqv_support_of = support_of) - - - security.declareProtected('MailManager Manage Tickets', 'headers') - def headers(self, REQUEST): - """Show or hide the message's raw headers.""" - if REQUEST.SESSION.has_key('show_headers'): - del REQUEST.SESSION['show_headers'] - else: - REQUEST.SESSION.set('show_headers', True) - return self.ticket_index_html(REQUEST) - - security.declareProtected('MailManager Manage Tickets', 'showHTML') - def showHTML(self, REQUEST): - """Should html_body be displayed?""" - show_html = REQUEST.SESSION.get('show_html') - if show_html is None: - return 0 - return show_html.get(self.absolute_url(), 0) - - security.declareProtected('MailManager Manage Tickets', 'toggleShowHTML') - def toggleShowHTML(self, REQUEST): - """Change whether html_body is displayed.""" - show_html = REQUEST.SESSION.get('show_html', {}) - if not show_html: - REQUEST.SESSION.set('show_html', show_html) - url = self.absolute_url() - show_html[url] = not show_html.get(url, 0) - return REQUEST.RESPONSE.redirect('%s/ticket/%06d' % (self.getBaseURL(), - self.id)) - - security.declareProtected('MailManager Manage Tickets', 'viewTicket') - def viewTicket(self): - """ User interface hook to note the ticket has been viewed - - We are currently tracking unread status in two places, once in - the ticket itself in sql (2.0 functionality) and once as an - attribute in the ruleset via hooks. The latter will be used in - future revisions to allow for more complex rulesets - """ - self.sql.markRead(sqv_id = self.id) - - # Generate and process the event - user = getSecurityManager().getUser().getUserName() - try: - self.engine.processEvent('ViewTicket', user, self) - except NoTransitionError: - pass - - transitions = True - while transitions: - try: - self.engine.processEvent('Epsilon', None, self) - print "Transition made" - except NoTransitionError: - print "No transition made" - transitions = False - - - security.declareProtected('MailManager Manage Tickets', 'export') - def export(self, RESPONSE): - """Export all of a ticket's messages in mbox format. TODO: export - the attachments too. This is probably going to involve creating - an email object to let it do the appropriate construction. - """ - RESPONSE.setHeader('content-type', 'text/plain') - RESPONSE.setHeader('Content-Disposition', - 'attachment; filename=mbox.txt') - write = RESPONSE.write # Saves a lookup every time - for msg in self.sql.listMessages(sqv_ticket_id = self.id): - date = DateTime(msg.msg_date).rfc822() - from_email = msg.from_email or self.from_email - write("From %s %s\n" % (from_email.encode('utf-8'), date)) - write(msg.raw_headers.encode('utf-8')) - write('\n\n') - for line in msg.body.splitlines(): - if from_escape.match(line): - line = u">" + line - write(line.encode('utf-8')) - write('\n') - write ('\n') - return - - security.declarePrivate('notifyUser') - def notifyUser(self, new_id=None): - """Email the assigned user that the ticket has been created/updated. - - By default notify the user the ticket is presently assigned to, if - new_id is set then notify the new user with that username. - """ - if self.state == 'Spam': - return - - log('%sNotifyUser alerting %s' % (self.getLogName(), new_id.encode('utf-8')), - logging.DEBUG, 'ticket.notify') - - mail_to = self.sql.listUsers(sqv_username=new_id or self.assigned)[0].email - # Guard against agent setting personal address to same as account's. - if mail_to == self.account_id: - return - - # Check to see if server_url and virtual_root are set. If so, use them. - # Otherwise, use getBaseURL(). Note that getBaseURL() may not work - # properly if called when the REQUEST object is not available. - if self.server_url: - if self.server_url.endswith('/'): - url = self.server_url + self.virtual_root - else: - url = self.server_url + '/' + self.virtual_root - else: - url = self.getBaseURL() - - zmsg = self.sql.listMessages(sqv_ticket_id=self.id)[-1] - body = u"""You have a new or updated ticket. - -From: %s -To: %s -Subject: %s -Full details: -%s/ticket/%06d""" % (zmsg.from_email, zmsg.msg_to, zmsg.subject, - url, self.id) - msg = self.createMessage(mail_to=mail_to, - mail_from=self.account_id, - subject=u'You have a new or updated ticket', - body=body)[0] - self.sendMessage(self.account_id, mail_to, msg) - - security.declarePrivate('notifyGroup') - def notifyGroup(self, group_name): - """Notify members of a group that a new mail has arrived. - - Do not notify the group member to whom the ticket has been assigned as - they will usually have been notified by the notifyUser() method. - """ - if self.state == 'Spam': - return - zmsg = self.sql.listMessages(sqv_ticket_id=self.id)[-1] - subject = u'%s has a new or updated ticket' % self.assigned - body = u"""%s has a new or updated ticket. - -From: %s -To: %s -Subject: %s -Full details: -%s/ticket/%06d""" % (self.assigned, zmsg.from_email, zmsg.msg_to, zmsg.subject, - self.absolute_url(), self.id) - - log('%sNotifying group memebers of %s' % (self.getLogName(), group_name.encode('utf-8')), - logging.DEBUG, 'ticket.notify') - - for member in self.sql.listGroupMembers(sqv_group_name=group_name): - log('%sNotifying user %s' % (self.getLogName(), member.username.encode('utf-8')), - logging.DEBUG, 'ticket.notify') - mail_to = self.sql.listUsers(sqv_username=member.username)[0].email - msg = self.createMessage(mail_to=mail_to, - mail_from=self.account_id, - subject=subject, - body=body)[0] - self.sendMessage(self.account_id, mail_to, msg) - - - ## Sending Methods #################################################### - # - # The following methods all handle sending replies from the user - # interface. - # - # http_sendReply \ - # http_sendAndClose \ sendMethod - addNote - # http_sendAndHold / - # http_addNote / - # - # The http methods are partially there for historical reasons, as these - # are what the ZPTs call. These in turn all call the refactored method - # sendMessage, adding information on what method made the call by - # passing the event and change_state variables. - # - # Replacing the http_ methods with a more generic method may cause - # problems by allowing the end user to manually submit with event or - # change_state variables. Permissions would need to be analysed before - # making this change. - # - - security.declareProtected('MailManager Manage Tickets', 'http_sendReply') - def http_sendReply(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=0, user_signature=None, next_id=None, - offset=None, last_modified=None, - REQUEST=None, RESPONSE=None): - """ Send a reply to a customer. """ - - return self.sendMethod(mail_to=mail_to, cc=cc, bcc=bcc, - subject=subject, body=body, body_is_html=body_is_html, - user_signature=user_signature, next_id=next_id, - last_modified=last_modified, offset=offset, - change_state=None, - event='SendReply', - REQUEST=REQUEST, RESPONSE=RESPONSE) - - security.declareProtected('MailManager Manage Tickets', 'http_sendAndClose') - def http_sendAndClose(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=0, user_signature=None, next_id=None, - offset=None, last_modified=None, - REQUEST=None, RESPONSE=None): - """ Send a reply and close the ticket. """ - - return self.sendMethod(mail_to=mail_to, cc=cc, bcc=bcc, - subject=subject, body=body, body_is_html=body_is_html, - user_signature=user_signature, next_id=next_id, - last_modified=last_modified, offset=offset, - change_state='Closed', - event='SendAndClose', - REQUEST=REQUEST, RESPONSE=RESPONSE) - - security.declareProtected('MailManager Manage Tickets', 'http_sendAndHold') - def http_sendAndHold(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=0, user_signature=None, next_id=None, - last_modified=None, offset=None, - REQUEST=None, RESPONSE=None): - """ Send a reply and put the ticket on hold. """ - - return self.sendMethod(mail_to=mail_to, cc=cc, bcc=bcc, - subject=subject, body=body, body_is_html=body_is_html, - user_signature=user_signature, next_id=next_id, - last_modified=last_modified, offset=offset, - change_state = 'Hold', - event = 'SendAndHold', - REQUEST=REQUEST, RESPONSE=RESPONSE) - - - security.declareProtected('MailManager Manage Tickets', 'http_addNote') - def http_addNote(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=False, user_signature=None, - last_modified=None, REQUEST=None, RESPONSE=None): - """ Add a note to a ticket. No mail is generated """ - - return self.sendMethod(mail_to=mail_to, cc=cc, bcc=bcc, - subject=subject, body=body, body_is_html=body_is_html, - user_signature=user_signature, sendmail=False, - last_modified=last_modified, - change_state = None, - event = 'AddNote', - REQUEST=REQUEST, RESPONSE=RESPONSE) - - security.declareProtected('MailManager Manage Tickets', 'http_Close') - def http_Close(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=0, user_signature=None, next_id=None, - offset=None, last_modified=None, - REQUEST=None, RESPONSE=None): - """ Close the ticket. - - This method is a little larger than the previous wrappers as it - does not actually send any message, so the combined sendMethod - cannot be used. - """ - # First, check for any modifications to the ticket - if last_modified: - mdate = str(self.sql.getTicketLastModified( - sqv_ticket_id = self.id)[0].last_modified.timeTime()) - if not last_modified == mdate: - REQUEST.set('error', 'This ticket has been modified. Please check it, and then retry your request') - return self.ticket_index_html(REQUEST) - - changed_by = getSecurityManager().getUser().getUserName() - if changed_by == 'Anonymous User': changed_by = '' - self.save(changed_by=changed_by, state='Closed', event='Close') - - # Redirect to the next ticket in the list, or just to the main - # tickets screen if no next ticket exists. - if RESPONSE is not None: - if next_id and offset: - RESPONSE.redirect('%s/ticket/%06d?offset:int=%d' % ( - self.getBaseURL(), next_id, offset)) - else: - RESPONSE.redirect('%s/Tickets' % self.getBaseURL()) - - - def sendMethod(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=False, user_signature=None, next_id=None, - last_modified=None, offset=None, change_state=None, - transition=None, event=None, sendmail=True, - REQUEST=None, RESPONSE=None): - """ Common underlying method for sending replies/adding notes - - This method handles all of the various http_ request method which - handle a user request to send a reply, or add a note to a ticket. - In order to avoid code duplication, this method takes care of - generating error messages, and sanitising user input, before - passing it to the addNote method to act on the request. - - - @param mail_to: The intended recipient - should be blank for notes - @param cc: Carbon Copy recipients - should be blank for notes - @param bcc: Blind Carbon Copy recipients - should be blank for notes - @type mail_to string (utf-8) - @type cc: string (utf-8) - @type bcc: string (utf-8) - @type subject: string (utf-8) - @type body_is_html: boolean - @type body: string (utf-8) - @type user_signature: string (utf-8) - @type next_id: int - @type last_modified: string (ascii, iso date format) - @type offset: int - @type change_state: string (utf-8) - @type transition: string (utf-8) - @type event: string (utf-8) - @type sendmail: boolean - """ - - # Sanitize the string parameters - # Parameters are in utf-8, convert to unicode to avoid problems - if mail_to is not None: mail_to = mail_to.decode('utf-8') - if cc is not None: cc = cc.decode('utf-8') - if bcc is not None: bcc = bcc.decode('utf-8') - if subject is not None: subject = subject.decode('utf-8') - if body is not None: body = body.decode('utf-8') - if user_signature is not None: user_signature = user_signature.decode('utf-8') - if change_state is not None: change_state= change_state.decode('utf-8') - if transition is not None: transition = transition.decode('utf-8') - if event is not None: event = event.decode('utf-8') - body_is_html = int(body_is_html) - - # First, check for any modifications to the ticket - if last_modified: - mdate = str(self.sql.getTicketLastModified( - sqv_ticket_id = self.id)[0].last_modified.timeTime()) - if not last_modified == mdate: - REQUEST.set('error', 'This ticket has been modified. Please check it, and then retry your request') - return self.ticket_index_html(REQUEST) - - if change_state: - if change_state == 'Closed' and not self._okToClose(): - if REQUEST is not None: - REQUEST.set('error', 'All supporting tickets must be Closed.') - REQUEST.set('flag_supporters', 1) - return self.ticket_index_html(REQUEST) - raise BadRequest, 'All supporting tickes must be Closed.' - - if sendmail: - if not mail_to: - if REQUEST is not None: - REQUEST.set('error', 'The To field may not be empty') - REQUEST.set('flag_mail_to', 1) - return self.ticket_index_html(REQUEST) - raise BadRequest, 'The To field may not be empty' - - if not self.validEmail(mail_to): - if REQUEST is not None: - REQUEST.set('error', '%s is not a valid email address.' % mail_to) - REQUEST.set('flag_mail_to', 1) - return self.ticket_index_html(REQUEST) - raise BadRequest, 'The To field may not be empty' - - else: - if mail_to or cc or bcc: - if REQUEST is not None: - REQUEST.set('error', - 'Private notes may not be sent to anyone.') - if mail_to: - REQUEST.set('flag_mail_to', True) - # Since the user probably didn't ask for this field to - # be filled in the first place helpfully remove it. - REQUEST.set('mail_to', '') - if cc: - REQUEST.set('flag_cc', True) - if bcc: - REQUEST.set('flag_bcc', True) - return self.ticket_index_html(REQUEST) - raise BadRequest, 'Private notes may not be sent to anyone.' - - # from_name will be the username of the logged in user. This will be - # stored in the database but will not be included in the outgoing mail. - from_name = getSecurityManager().getUser().getUserName() - - # Get attachments out of the session - if REQUEST is not None: - attachments = REQUEST.SESSION.get('attachments', {}) - if attachments: - del REQUEST.SESSION['attachments'] - else: - attachments = {} - - if REQUEST is not None: - raw_headers = 'X-IP-Address: %s' % REQUEST.REMOTE_ADDR - remote_addr = REQUEST.REMOTE_ADDR - else: - raw_headers = '' - remote_addr = None - - # Generate and process the event - # Catch security and permissions violations - try: - if event: - user = getSecurityManager().getUser().getUserName() - self.engine.processEvent(event, user, self) - - transitions = True - while transitions: - try: - self.engine.processEvent('Epsilon', None, self) - print "Transition made" - except NoTransitionError: - print "No transition made" - transitions = False - except SecurityError, e: - - get_transaction().abort() - - error_msg = 'Your attempt to send this message failed due to a security ' - error_msg += 'failure: %s ' % str(e) - - if REQUEST is not None: - REQUEST.set('error', error_msg) - return self.ticket_index_html(REQUEST) - else: - raise BadRequest, error_msg - - try: - self.addNote(mail_to = mail_to, cc = cc, bcc = bcc, subject = subject, - body = body, body_is_html = body_is_html, - raw_headers = raw_headers, - attachments = attachments, from_name = from_name, - user_signature = user_signature, sendmail=sendmail, - change_state = change_state, remote_addr = remote_addr, - transition = transition, event = event) - except SMTPRecipientsRefused, e: - - get_transaction().abort() - - error_msg = 'The mailserver returned a failure whilst trying to ' - error_msg += 'send this message. Check your mail addresses are correct. ' - error_msg += 'The exact error recieved is %s ' % str(e) - - if REQUEST is not None: - REQUEST.set('error', error_msg) - REQUEST.set('flag_mail_to', True) - REQUEST.set('flag_cc', True) - REQUEST.set('flag_bcc', True) - return self.ticket_index_html(REQUEST) - else: - raise BadRequest, error_msg - - # Actions complete, either show the updated ticket, or in the case - # of tickets with modified state, redirect to the next ticket - - if change_state == 'Closed' or change_state == 'Hold': - # Redirect to the next ticket in the list, or just to the main - # tickets screen if no next ticket exists. - if RESPONSE is not None: - if next_id and offset: - RESPONSE.redirect('%s/ticket/%06d?offset:int=%d' % ( - self.getBaseURL(), next_id, offset)) - else: - RESPONSE.redirect('%s/Tickets' % self.getBaseURL()) - - else: - # Redirect to show the updated ticket - if RESPONSE is not None: - return RESPONSE.redirect('%s/ticket/%06d' % (self.getBaseURL(), - self.id)) - - - - security.declareProtected('MailManager Manage Tickets', 'addNote') - def addNote(self, mail_to, cc='', bcc='', subject='', body='', - body_is_html=0, attachments = {}, from_name = '', - user_signature=None, sendmail=0, raw_headers = '', - change_state = None, last_modified=None, - remote_addr = None, transition=None, event=None): - """ Add a copy of a message to a ticket. - - Also actually send the message to the customer, unless the - message is just a private note. - - Note that the signature is added to the message before it is added - to sql, and that is is not added before being passed to createMessage. - This is because the createMessage method adds on a signature on its - own accord. This should eventually be refactored, so that the message - being saved to the db and the message generated by createMessage are - one and the same. - - This method - - """ - - log('%sAdding message to ticket' % (self.getLogName()), - logging.DEBUG, 'ticket.addnote') - - #if change_state: - # # Forcably set the new state. - # changed_by = getSecurityManager().getUser().getUserName() - # if changed_by == 'Anonymous User': changed_by = '' - # self.save(changed_by=changed_by, state=change_state, event=event) - - # The first time we reply check promptness. - if not self.date_responded: - self.sql.setDateResponded(sqv_id=self.id) - - # Get the account signature - account = self.sql.listAccounts(sqv_email=self.account_id)[0] - if account.signature: - account_signature = (account.signature_text, - account.html_signature) - else: - account_signature = None - - - # Obtain body or html_body with appended signatures - if body_is_html: - log('%sSanitising HTML body' % (self.getLogName()), - logging.DEBUG, 'ticket.addnote') - body, html_body = '', body - body_with_sig = body - html_body_with_sig = self._addSignature(html_body, user_signature, - account_signature, html=1) - else: - html_body = '' - body_with_sig = self._addSignature(body, user_signature, account_signature) - html_body_with_sig = '' - - # Get an SQL id for this message. Note that the SQL ids are sequential - # ids to identify each message in mailmanager. The message_id otoh, is - # there to uniquely identify the message globally. They are entirely - # independant fields in the SQL database. - msg_id = self.sql.getNextMessageId()[0].id - - # Generate a message id for this message - message_id = self.generateMessageId(self.id, msg_id) - - log('%sStoring message in database' % (self.getLogName()), - logging.DEBUG, 'ticket.addnote') - - self.sql.addMessage(sqv_id=msg_id, - sqv_ticket_id=self.id, - sqv_message_id=message_id, - sqv_from_name=from_name, - sqv_from_email=self.account_id, - sqv_subject=subject, - sqv_msg_to=mail_to, - sqv_cc=cc, - sqv_bcc=bcc, - sqv_reply_to='', - sqv_raw_headers=raw_headers, - sqv_body=body_with_sig, - sqv_html_body=html_body_with_sig) - - for attach in attachments.values(): - # IE sets the filename to being the full path - title=attach['filename'].split('\\')[-1] - log('%sAdding attachment %s' % (self.getLogName(), title), - logging.DEBUG, 'ticket.addnote') - - if self.dbplatform == 'postgres': - self.sql.addAttachment(sqv_message_id=msg_id, - sqv_title=title, - sqv_content_type=attach['content-type'], - sqv_is_file=self.sql_truevar, - sqv_body=Binary(attach['data']), - sqv_dangerous=self.sql_falsevar) - else: - self.sql.addAttachment(sqv_message_id=msg_id, - sqv_title=title, - sqv_content_type=attach['content-type'], - sqv_is_file=self.sql_truevar, - sqv_body=attach['data'], - sqv_dangerous=self.sql_falsevar) - - if sendmail: - # A very irritating hack. The ZSQL methods will not accept None as - # a null so we need to use '' in sql.addAttachment. However the - # createMessage has to be able to distinguish between an empty - # body/html_body (body='', html_body='') and the body/html_body not - # present (body=None, html_body=None). This can be removed when the - # following Zope bug is fixed: - # http://www.zope.org/Collectors/Zope/556 - if body_is_html: - body = None - else: - html_body = None - msg, recipients = self.createMessage(mail_to=mail_to, - mail_from=self.account_id, - attach=attachments, - cc=cc, - bcc=bcc, - subject=subject, - body=body, - html_body=html_body, - user_signature=user_signature, - account_signature=account_signature, - message_id=message_id, - remote_addr = remote_addr) - - log('%sSending outgoing message' % (self.getLogName()), - logging.DEBUG, 'ticket.addnote') - - self.sendMessage(self.account_id, - reduce(operator.add, recipients.values()), - msg) - - - security.declareProtected('MailManager Manage Tickets', 'http_returnToQueue') - def http_returnToQueue(self, mail_to, cc='', bcc='', subject='', - body='', body_is_html=0, user_signature=None, - sendmail=0, next_id=None, last_modified=None, - offset=None, REQUEST=None, RESPONSE=None): - """ Returns the current ticket to the queue - - This method returns the current ticket to the queue. It then - redirects to the next ticket, or to the tickets list should - no next ticket exist. - """ - # First, check for any modifications to the ticket - if last_modified: - mdate = str(self.sql.getTicketLastModified( - sqv_ticket_id = self.id)[0].last_modified.timeTime()) - if not last_modified == mdate: - REQUEST.set('error', 'This ticket has been modified. Please check it, and then retry your request') - return self.ticket_index_html(REQUEST) - - # Generate and process the event - user = getSecurityManager().getUser().getUserName() - self.engine.processEvent('ReturnToQueue', user, self) - - transitions = True - while transitions: - try: - self.engine.processEvent('Epsilon', None, self) - print "Transition made" - except NoTransitionError: - print "No transition made" - transitions = False - - # Redirect to next ticket or to the tickets list - if RESPONSE is not None: - if next_id is not None and offset is not None: - RESPONSE.redirect('%s/ticket/%06d?offset:int=%d' % ( - self.getBaseURL(), next_id, offset)) - else: - RESPONSE.redirect('%s/Tickets' % self.getBaseURL()) - - - def getDefaultTemplate(self, REQUEST): - """ Method to set the default template in the ticket response - - This method is called from the ticket index page. It will set the - default template for replies, providing that no template has been - set already. This method will translate the template_name - and possibly update the body and body_is_html request variables - """ - - # Check to see if a template name is already set - if REQUEST.get('template_name', None) is not None: - return unicode(REQUEST['template_name'], 'utf-8') - - # Otherwise, this is a new reponse, set the template automatically - else: - - # Template has not already been set, get the default - account = self.sql.listAccounts(sqv_email=self.account_id)[0] - template_name = account.default_template - - # Obtain the template details, deailing with the cite_last - # method appropriately - if template_name == 'cite_last': - - # Find the last actual message - msgs = self.sql.listMessages(sqv_ticket_id=self.id) - # Skip over any notes - for position in range(1,len(msgs)+1): - zmsg = msgs[-position] - if not zmsg.msg_to == '': break - # Remove the signature and split the plain text body per - # line, start with > for quote chars. Note that we don't - # currently quote HTML, but that would be a nice addition - # in the future. - cite = sig_remover.sub('', zmsg.body) - body = u''.join(['> %s' % line for line - in cite.splitlines(1)]) - template_is_html = 0 - - elif template_name is not None and not (template_name == ''): - template = self._getTemplate(template_name) - body = template['body'] - template_is_html = template['html'] - - else: - template_is_html = False - template_name = u'' - body = u'' - - # Is this an HTML template? Check the settings flag - if template_is_html: - # Set the body_is_html flag so that the correct editor is - # used for the reply - REQUEST.set('body_is_html', True) - else: - # If HTMLRequired is set, convert to HTML for the editing pane - # Fixes bug - Formatting broken on templates for HTML (#1477664) - if self.HTMLRequired(): - body = self._makeHTML(body) - REQUEST.set('body_is_html', True) - else: - REQUEST.set('body_is_html', False) - - REQUEST.set('body', body) - - # Return value of 'No Change' to UI - return '' - - - def http_setTemplate(self, REQUEST): - """ Handler for user selecting a new template to use in a message - - This method is called when the end user clicks the arrow button - to the right of the template. This method then replaces the - content of the current message body with the given template, - setting the editor to HTML/plain text as appropriate. If - template_name is None then do nothing. - - Request variables: - - * template_name : the template name we are to replace with - * body_is_html : the format to encode the resulting body in - * body : altered to be the content of the given tempalte - """ - template_name = REQUEST.get('template_name', '').decode('utf-8') - - if template_name: - - # Obtain the template details, deailing with the cite_last - # method appropriately - if template_name == 'cite_last': - body = self._citeLastMessage() - template_is_html = 0 - else: - template = self._getTemplate(template_name) - # Replace template paramaters - body = self.replaceTemplateParameters(template['body']) - template_is_html = template['html'] - - # Is this an HTML template? Check the settings flag - if template_is_html: - # Set the body_is_html flag so that the correct editor is - # used for the reply - REQUEST.set('body_is_html', True) - else: - # If HTMLRequired is set, convert to HTML for the editing pane - # Fixes bug - Formatting broken on templates for HTML (#1477664) - if self.HTMLRequired(): - body = self._makeHTML(body) - REQUEST.set('body_is_html', True) - else: - REQUEST.set('body_is_html', False) - - REQUEST.set('body', body) - REQUEST.set('autojump', True) - - return self.index_html(REQUEST) - - security.declareProtected('MailManager Manage Tickets', '_citeLastMessage') - def _citeLastMessage(self): - """ Obtain a citation of the last message - - Returns a utf-8 encoded string - """ - - # Find the last actual message - msgs = self.sql.listMessages(sqv_ticket_id=self.id) - # Skip over any notes - for position in range(1,len(msgs)+1): - zmsg = msgs[-position] - if not zmsg.msg_to == '': break - # Remove the signature and split the plain text body per - # line, start with > for quote chars. Note that we don't - # currently quote HTML, but that would be a nice addition - # in the future. - cite = sig_remover.sub('', zmsg.body) - body = ''.join(['> %s' % line.encode('utf-8') for line - in cite.splitlines(1)]) - return body - - security.declareProtected('MailManager Manage Tickets', 'replaceTemplateParameters') - def replaceTemplateParameters(self, template_body): - """ Replace the metavariables in the templates with the right content - - Currently supported is: - - %T - ticket id - %L - cite last message - """ - # Replace any paramaters in this code - template_body = template_body.replace('%T', str(self.id)) - template_body = template_body.replace('%L', self._citeLastMessage()) - return template_body - - - security.declareProtected('MailManager Manage Tickets', 'renderHTML') - def renderHTML(self, msg_id): - """ Temporary method to render a message as an iframe """ - return u""" - <iframe src="ticket/%i/messageContent?msg_id=%i" width="100%%" - style="height:25.0em" scrolling="no" - marginwidth="0" marginheight="0"> - </iframe> - """ % (self.id, msg_id) - - security.declareProtected('MailManager Manage Tickets', 'messageContent') - def messageContent(self, msg_id, REQUEST): - """ Returns the content of an HTML message as an HTML page """ - msg = self.sql.listMessages(sqv_sql_id=msg_id)[0] - return self.MessageTemplate(messageContent=msg.html_body) - - def http_setHTML(self, REQUEST): - """ Handler for user changing the format of a message body - - This method is called when the end user clicks the arrow button - to the right of the HTML/Plain text option. This method then - replaces the content of the current message body based on - given template, converting to HTML/plain text as appropriate. - - Request variables: - - * new_body_format: the format to encode the resulting body in - * body_is_html : gets set to the format the body is now in - * body : altered so that it is in the correct format - """ - body_is_html = int(REQUEST.get('body_is_html', False)) - new_body_html = int(REQUEST.get('new_body_html', False)) - body = REQUEST.get('body', '').decode('utf-8') - - if body_is_html and not new_body_html: - # Convert from html to text - body = html2text(body) - - if not body_is_html and new_body_html: - # Convert from text to html - body = self._makeHTML(body) - - REQUEST.set('body', body.encode('utf-8')) - REQUEST.set('body_is_html', new_body_html) - REQUEST.set('autojump', True) - - return self.index_html(REQUEST) - - security.declarePrivate('_getTemplate') - def _getTemplate(self, template_name): - templates = self.sql.getTemplate(sqv_name = template_name) - if len(templates) > 0: - return templates[0] - else: - return {} - - security.declareProtected('MailManager Manage Tickets', 'HTMLRequired') - def HTMLRequired(self): - """Determine whether the message must be composed in HTML. - - Currently HTML is required when an HTML account signature is - in use. Once HTML user signatures are added they will also - force HTML composition. - """ - account = self.sql.listAccounts(sqv_email=self.account_id)[0] - return account.signature and account.html_signature - - security.declareProtected('MailManager Manage Tickets', 'getTo') - def getTo(self): - """Get the email address for the To: line. - - Format the name and email address nicely, eg: - Andrew Veitch <an...@lo...> - """ - return formataddr( (self.from_name, self.from_email) ) - - security.declareProtected('MailManager Manage Tickets', 'formatAddr') - def formatAddr(self, addrtuple): - """Format an address pair (name, email) for reply - - This is here purely so the ticket_index_html.zpt file can reference - the email.Utils package method. - """ - return formataddr( addrtuple ) - - security.declareProtected('MailManager Manage Tickets', 'getReplyTo') - def getReplyTo(self): - """Get the Reply-To header from the first message in the ticket. - - Used instead of From: as the address to reply to, if present. - """ - zmsg = self.sql.listMessages(sqv_ticket_id=self.id)[0] - return zmsg.reply_to - - security.declareProtected('MailManager Manage Tickets', 'getReplyAll') - def getReplyAll(self): - """Get the address for Reply to All. - - Include addresses in the To field and in the Cc field of the original - mail. Do not return duplicate addresses. Only the actual email portion - of the address should be returned. - """ - zmsg = self.sql.listMessages(sqv_ticket_id=self.id)[0] - emails = [parseaddr(addr)[1] - for addr in (self._tokenizer(zmsg.msg_to) + - self._tokenizer(zmsg.cc)) - if addr] - # We only want to copy in extra To addresses so ignore the account id - # and ignore the user's own address in case they copied themselves into - # their original email. - if self.account_id in emails: - emails.remove(self.account_id) - if self.from_email in emails: - emails.remove(self.from_email) - # Make unique - return ', '.join(dict.fromkeys(emails).keys()) - - security.declareProtected('MailManager Manage Tickets', 'addAttachment') - def addAttachment(self, REQUEST, RESPONSE, standard_attach = None, - file_attach=None): - """Add an attachment to a reply. - - The attachment can either be one of the standard attachments or an - uploaded file. Attachments are stored in the SESSION until the message - is sent. - """ - attachments = REQUEST.SESSION.get('attachments', {}) - if standard_attach: - file = self.attachments[standard_attach] - attachments[standard_attach] = {'filename': standard_attach, - 'content-type': file.content_type, - 'data': file.manage_FTPget()} - if file_attach: - file = {'filename': file_attach.filename, - 'content-type': - file_attach.headers.get('content-type', - 'application/octet-stream'), - 'data': file_attach.read()} - attachments[file_attach.filename] = file - REQUEST.SESSION.set('attachments', attachments) - REQUEST.set('autojump', True) - return self.ticket_index_html(REQUEST) - - security.declareProtected('MailManager Manage Tickets', 'delAttachment') - def delAttachment(self, REQUEST, RESPONSE, ids=[]): - """Delete an attachment.""" - for id in ids: - attachments = REQUEST.SESSION['attachments'] - del attachments[id] - REQUEST.SESSION.set('attachments', attachments) - return self.ticket_index_html(REQUEST) - - security.declareProtected('MailManager Manage Tickets', 'getPrevNext') - def getPrevNext(self, request): - """ - Get the previous and next ticket ids. The supplied variable offset - is the current position in the list of tickets. We retreive the - ticket previous to the offset and the one after. Return value is - a tuple containing the previous ticket id, and the next ticket id. - """ - offset = request.get('offset') - if offset is None: - return (None, None) - - if offset == 0: - # If we are at the start of the result set, the previous ticket - # does not exist. Only retreive two tickets. - results = self.listTickets(request, offset=0, limit=2) - if not results: - return (None, None) - - if len(results) == 2: - return (None, results[1].id) - else: - return (None, None) - - else: - # We are in the middle of a result set, so we want to try and - # retrieve three tickets (previous, current, next) - results = self.listTickets(request, offset=max(offset-1, 0), limit=3) - if not results: - return (None, None) - - if len(results) == 3: - return(results[0].id, results[2].id) - elif len(results) > 0: - return(results[0].id, None) - else: - return (None, None) - - ######################################################################### - # - # Information methods - # - # The following methods are called from ZPTs in order to get information - # about the current ticket - # - - security.declareProtected('MailManager Manage Tickets', 'getHistory') - def getHistory(self): - """ Returns a structure which can be used to give a human readable - history of a ticket's lifecycle. - - @return: list of dictionarys ('string' : 'struct') - - FIXME: This needs support for i18n added - - The results of this method are shown on the side of the ticket - display. This shows a list of history items, each with a number - of key:value entries depicting what was changed. The value of - each of the entries is a struct, allowing us to generate img - tags or similar if necessary. - - State - Subject - Assigned - Priority - Category - Support of - Changed - - The rul... [truncated message content] |