source: trunk/email2trac.py.in @ 545

Last change on this file since 545 was 545, checked in by bas, 13 years ago

added email2trac_workflow function. So we now honor workflow for new tickets and ticket updates, see #252, #226

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 81.8 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[22]44
[282]45    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
46    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]47       
48 * default config file is : /etc/email2trac.conf
49
50 * Commandline opions:
[205]51                -h,--help
[507]52                -d, --debug
[205]53                -f,--file  <configuration file>
[410]54                -n,--dry-run
[205]55                -p, --project <project name>
[415]56                -t, --ticket_prefix <name>
[22]57
58SVN Info:
59        $Id: email2trac.py.in 545 2011-08-10 10:18:09Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import time
66import email
[136]67import email.Iterators
68import email.Header
[22]69import re
70import urllib
71import unicodedata
72import mimetypes
[96]73import traceback
[403]74import logging
75import logging.handlers
[404]76import UserDict
[498]77import tempfile
[433]78
[520]79from datetime import timedelta, datetime
[433]80from stat import *
[22]81
[403]82
[363]83from trac import __version__ as trac_version
[199]84from trac import config as trac_config
[91]85
[508]86## Some global variables
[96]87#
88m = None
[22]89
[508]90# This is to for the function AlwaysNotifyReporter
91sender_email = None
92
[404]93class SaraDict(UserDict.UserDict):
[507]94    def __init__(self, dictin = None):
95        UserDict.UserDict.__init__(self)
96        self.name = None
97       
98        if dictin:
99            if dictin.has_key('name'):
100                self.name = dictin['name']
101                del dictin['name']
102            self.data = dictin
103           
104    def get_value(self, name):
105        if self.has_key(name):
106            return self[name]
107        else:
108            return None
109               
110    def __repr__(self):
111        return repr(self.data)
[182]112
[507]113    def __str__(self):
114        return str(self.data)
115           
116    def __getattr__(self, name):
117        """
118        override the class attribute get method. Return the value
119        from the dictionary
120        """
121        if self.data.has_key(name):
122            return self.data[name]
123        else:
124            return None
125           
126    def __setattr__(self, name, value):
127        """
128        override the class attribute set method only when the UserDict
129        has set its class attribute
130        """
131        if self.__dict__.has_key('data'):
132            self.data[name] = value
133        else:
134            self.__dict__[name] = value
[404]135
[507]136    def __iter__(self):
137        return iter(self.data.keys())
[404]138
[22]139class TicketEmailParser(object):
[507]140    env = None
141    comment = '> '
[359]142
[507]143    def __init__(self, env, parameters, logger, version):
144        self.env = env
[22]145
[507]146        # Database connection
147        #
148        self.db = None
[22]149
[507]150        # Save parameters
151        #
152        self.parameters = parameters
153        self.logger = logger
[206]154
[507]155        # Some useful mail constants
156        #
157        self.email_name = None
158        self.email_addr = None
159        self.email_from = None
160        self.author     = None
161        self.id         = None
162       
163        self.STRIP_CONTENT_TYPES = list()
[72]164
[507]165        ## fields properties via body_text
166        #
167        self.properties = dict()
[452]168
[507]169        self.VERSION = version
[402]170
[507]171        self.get_config = self.env.config.get
[404]172
[507]173        ## init function ##
174        #
175        self.setup_parameters()
[402]176
[507]177    def setup_parameters(self):
178        if self.parameters.umask:
179            os.umask(self.parameters.umask)
[402]180
[507]181        if not self.parameters.spam_level:
182            self.parameters.spam_level = 0
[22]183
[507]184        if not self.parameters.spam_header:
185            self.parameters.spam_header = 'X-Spam-Score'
[207]186
[507]187        if not self.parameters.email_quote:
188            self.parameters.email_quote = '> '
[22]189
[507]190        if not self.parameters.ticket_update_by_subject_lookback:
191            self.parameters.ticket_update_by_subject_lookback = 30
[360]192
[507]193        if self.parameters.verbatim_format == None:
194            self.parameters.verbatim_format = 1
[118]195
[507]196        if self.parameters.reflow == None:
197            self.parameters.reflow = 1
[231]198
[507]199        if self.parameters.binhex:
200            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]201
[507]202        if self.parameters.applesingle:
203            self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]204
[507]205        if self.parameters.appledouble:
206            self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]207
[507]208        if self.parameters.strip_content_types:
209            items = self.parameters.strip_content_types.split(',')
210            for item in items:
211                self.STRIP_CONTENT_TYPES.append(item.strip())
[294]212
[507]213        if self.parameters.tmpdir:
214            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
215        else:
216            self.parameters.tmpdir = os.path.normcase('/tmp')
[257]217
[507]218        if self.parameters.email_triggers_workflow == None:
219            self.parameters.email_triggers_workflow = 1
[191]220
[507]221        if not self.parameters.subject_field_separator:
222            self.parameters.subject_field_separator = '&'
223        else:
224            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
[297]225
[507]226        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
227        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
[305]228
[484]229
[507]230        self.system = None
[359]231
[341]232########## Email Header Functions ###########################################################
[339]233
[507]234    def spam(self, message):
235        """
236        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
237        # Note if Spam_level then '*' are included
238        """
239        spam = False
240        if message.has_key(self.parameters.spam_header):
241            spam_l = string.split(message[self.parameters.spam_header])
[22]242
[507]243            try:
244                number = spam_l[0].count('*')
245            except IndexError, detail:
246                number = 0
247               
248            if number >= self.parameters.spam_level:
249                spam = True
250               
251        # treat virus mails as spam
252        #
253        elif message.has_key('X-Virus-found'):         
254            spam = True
[194]255
[507]256        # How to handle SPAM messages
257        #
258        if self.parameters.drop_spam and spam:
[194]259
[507]260            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
261            return 'drop'   
[194]262
[507]263        elif spam:
[194]264
[507]265            return 'Spam'   
266        else:
[22]267
[507]268            return False
[191]269
[507]270    def email_header_acl(self, keyword, header_field, default):
271        """
272        This function wil check if the email address is allowed or denied
273        to send mail to the ticket list
274        """
275        self.logger.debug('function email_header_acl: %s' %keyword)
[409]276
[507]277        try:
278            mail_addresses = self.parameters[keyword]
[221]279
[507]280            # Check if we have an empty string
281            #
282            if not mail_addresses:
283                return default
[221]284
[507]285        except KeyError, detail:
286            self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
[206]287
[507]288            return default
[206]289
[507]290        mail_addresses = string.split(mail_addresses, ',')
[221]291
[507]292        for entry in mail_addresses:
293            entry = entry.strip()
294            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
295            result =  TO_RE.search(header_field)
296            if result:
297                return True
[149]298
[507]299        return False
[208]300
[507]301    def email_header_txt(self, m):
302        """
303        Display To and CC addresses in description field
304        """
305        s = ''
[334]306
[507]307        if m['To'] and len(m['To']) > 0:
308            s = "'''To:''' %s\r\n" %(m['To'])
309        if m['Cc'] and len(m['Cc']) > 0:
310            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]311
[507]312        return  self.email_to_unicode(s)
[22]313
[138]314
[507]315    def get_sender_info(self, message):
316        """
317        Get the default author name and email address from the message
318        """
[43]319
[507]320        self.email_to = self.email_to_unicode(message['to'])
321        self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
[226]322
[507]323        self.email_from = self.email_to_unicode(message['from'])
324        self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]325
[507]326        ## Trac can not handle author's name that contains spaces
327        #
[518]328        if self.email_addr.lower() == self.trac_smtp_from.lower():
[507]329            if self.email_name:
330                self.author = self.email_name
331            else:
332                self.author = "email2trac"
333        else:
334            self.author = self.email_addr
[304]335
[507]336        if self.parameters.ignore_trac_user_settings:
337            return
[194]338
[507]339        # Is this a registered user, use email address as search key:
340        # result:
341        #   u : login name
342        #   n : Name that the user has set in the settings tab
343        #   e : email address that the user has set in the settings tab
344        #
345        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
346            if (
347                (e and (e.lower() == self.email_addr.lower())) or
[518]348                (u.lower() + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
[507]349            )
350            ]
[43]351
[507]352        if len(users) >= 1:
353            self.email_from = users[0][0]
354            self.author = users[0][0]
[45]355
[507]356    def set_reply_fields(self, ticket, message):
357        """
358        Set all the right fields for a new ticket
359        """
360        self.logger.debug('function set_reply_fields')
[72]361
[507]362        ## Only use name or email adress
363        #ticket['reporter'] = self.email_from
364        ticket['reporter'] = self.author
[270]365
366
[507]367        # Put all CC-addresses in ticket CC field
368        #
369        if self.parameters.reply_all:
[43]370
[507]371            email_cc = ''
[299]372
[507]373            cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
[299]374
[507]375            if not cc_addrs:
376                return
[43]377
[507]378            ## Build a list of forbidden CC addresses
379            #
380            #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
381            #to_list = list()
382            #for n,e in to_addrs:
383            #   to_list.append(e)
384               
385            # Always Remove reporter email address from cc-list
386            #
387            try:
388                cc_addrs.remove((self.author, self.email_addr))
389            except ValueError, detail:
390                pass
[43]391
[507]392            for name,addr in cc_addrs:
393       
394                ## Prevent mail loop
395                #
396                #if addr in to_list:
[304]397
[507]398                if addr == self.trac_smtp_from:
399                    self.logger.debug("Skipping %s mail address for CC-field" %(addr))
400                    continue
[43]401
[507]402                if email_cc:
403                    email_cc = '%s, %s' %(email_cc, addr)
404                else:
405                    email_cc = addr
[96]406
[507]407            if email_cc:
408                self.logger.debug('set_reply_fields: %s' %email_cc)
[299]409
[507]410                ticket['cc'] = self.email_to_unicode(email_cc)
[299]411
[339]412
413########## DEBUG functions  ###########################################################
414
[507]415    def debug_body(self, message_body, temporary_file=False):
416        if temporary_file:
[528]417            (fd, body_file) = tempfile.mkstemp('.body_email2trac')
[514]418            fx = os.fdopen(fd, 'wb')
[507]419        else:
420            body_file = os.path.join(self.parameters.tmpdir, 'body.txt')
[514]421            fx = open(body_file, 'wb')
[310]422
[507]423        if self.parameters.dry_run:
424            print 'DRY-RUN: not saving body to %s' %(body_file)
425            return
[331]426
[507]427        print 'writing body to %s' %(body_file)
428        if not message_body:
429                message_body = '(None)'
[310]430
[507]431        message_body = message_body.encode('utf-8')
432        #message_body = unicode(message_body, 'iso-8859-15')
[310]433
[507]434        fx.write(message_body)
435        fx.close()
436        try:
437            os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
438        except OSError:
439            pass
[310]440
[507]441    def debug_attachments(self, message_parts):
442        """
443        """
444        self.logger.debug('function debug_attachments')
445       
446        n = 0
447        for item in message_parts:
448            # Skip inline text parts
449            if not isinstance(item, tuple):
450                continue
451               
452            (original, filename, part) = item
[310]453
[507]454            n = n + 1
455            print 'part%d: Content-Type: %s' % (n, part.get_content_type())
456       
457            s = 'part%d: filename: %s' %(n, filename)
458            self.print_unicode(s)
459   
460            ## Forbidden chars
461            #
462            filename = filename.replace('\\', '_')
463            filename = filename.replace('/', '_')
[528]464            filename = filename + '.att_email2trac'
[507]465   
466            part_file = os.path.join(self.parameters.tmpdir, filename)
467            s = 'writing part%d (%s)' % (n,part_file)
468            self.print_unicode(s)
[331]469
[507]470            if self.parameters.dry_run:
471                print 'DRY_RUN: NOT saving attachments'
472                continue
[331]473
[507]474            part_file = util.text.unicode_quote(part_file)
[377]475
[507]476            fx = open(part_file, 'wb')
477            text = part.get_payload(decode=1)
[331]478
[507]479            if not text:
480                text = '(None)'
[331]481
[507]482            fx.write(text)
483            fx.close()
[331]484
[507]485            try:
486                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
487            except OSError:
488                pass
[310]489
[528]490    def save_email_for_debug(self, message, project_name, create_tempfile=False):
[309]491
[528]492        if self.parameters.dry_run:
493            print 'DRY_RUN: NOT saving email message'
494            return
495
[507]496        if create_tempfile:
[528]497            (fd, msg_file) = tempfile.mkstemp('.%s.email2trac' %project_name)
[514]498            fx = os.fdopen(fd, 'wb')
[507]499        else:
500            #msg_file = '/var/tmp/msg.txt'
[528]501            msg_file = os.path.join(self.parameters.tmpdir, 'msg.%s.email2trac' %project_name)
[514]502            fx = open(msg_file, 'wb')
[173]503
[44]504
[528]505        print 'saving email to %s' %(msg_file)
506        fx.write('%s' % message)
507        fx.close()
508       
509        try:
510            os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
511        except OSError:
512            pass
[331]513
[507]514        message_parts = self.get_message_parts(message)
515        message_parts = self.unique_attachment_names(message_parts)
516        body_text = self.get_body_text(message_parts)
517        self.debug_body(body_text, True)
518        self.debug_attachments(message_parts)
[309]519
[339]520########## Conversion functions  ###########################################################
521
[507]522    def email_to_unicode(self, message_str):
523        """
524        Email has 7 bit ASCII code, convert it to unicode with the charset
525        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
526        understands it.
527        """
528        self.logger.debug("function email_to_unicode")
[341]529
[507]530        results =  email.Header.decode_header(message_str)
[341]531
[507]532        s = None
533        for text,format in results:
534            if format:
535                try:
536                    temp = unicode(text, format)
537                except UnicodeError, detail:
538                    # This always works
539                    #
[525]540                    msg = 'ERROR: Could not find charset: %s, please install' %format
541                    self.logger.error(msg)
[507]542                    temp = unicode(text, 'iso-8859-15')
543                except LookupError, detail:
[514]544                    msg = 'ERROR: Could not find charset: %s, please install' %format
[525]545                    self.logger.error(msg)
[507]546                    #temp = unicode(text, 'iso-8859-15')
547                    temp = message_str
548                       
549            else:
550                temp = string.strip(text)
551                temp = unicode(text, 'iso-8859-15')
[341]552
[507]553            if s:
554                s = '%s %s' %(s, temp)
555            else:
556                s = '%s' %temp
[341]557
[507]558        #s = s.encode('utf-8')
559        return s
[341]560
[507]561    def str_to_dict(self, s):
562        """
563        Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
564        """
565        self.logger.debug("function str_to_dict")
[164]566
[507]567        fields = string.split(s, self.parameters.subject_field_separator)
[262]568
[507]569        result = dict()
570        for field in fields:
571            try:
572                index, value = string.split(field, '=')
[169]573
[507]574                # We can not change the description of a ticket via the subject
575                # line. The description is the body of the email
576                #
577                if index.lower() in ['description']:
578                    continue
[169]579
[507]580                if value:
581                    result[index.lower()] = value
[169]582
[507]583            except ValueError:
584                pass
585        return result
[167]586
[507]587    def print_unicode(self,s):
588        """
589        This function prints unicode strings if possible else it will quote it
590        """
591        try:
592            self.logger.debug(s)
593        except UnicodeEncodeError, detail:
594            self.logger.debug(util.text.unicode_quote(s))
[379]595
[503]596
[507]597    def html_2_txt(self, data):
598        """
599        Various routines to convert html syntax to valid trac wiki syntax
600        """
601        self.logger.debug('function html_2_txt')
[503]602
[507]603        ## This routine make an safe html that can be include
604        #  in trac, but no further text processing can be done
605        #
606#       try:
607#           from lxml.html.clean import Cleaner
608#           tags_rm = list()
609#           tags_rm.append('body')
[503]610#
[507]611#           cleaner = Cleaner(remove_tags=tags_rm )
612#           parsed_data = cleaner.clean_html(data)
613#           parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
[503]614#
[507]615#           return parsed_data
616#           
617#       except ImportError::
618#           pass
[503]619
[507]620        parsed_data = None
621        if self.parameters.html2text_cmd:
[514]622            (fd, tmp_file) = tempfile.mkstemp('email2trac.html')
623            f = os.fdopen(fd, 'w')
624
[507]625            cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
626            self.logger.debug('\t html2text conversion %s'%(cmd))
627   
628            if self.parameters.dry_run:
629                print 'DRY_RUN: html2text conversion command: %s\n' %(cmd)
[503]630
[507]631            else:
632                f.write(data)
633                f.close()
[503]634
[507]635                lines = os.popen(cmd).readlines()
636                parsed_data =  ''.join(lines)
[503]637
[507]638                os.unlink(tmp_file)
[503]639
[507]640        else:
641            self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
[503]642
[507]643        return parsed_data
[503]644
[339]645########## TRAC ticket functions  ###########################################################
646
[545]647    def email2trac_workflow(self, tkt):
648        """
649        """
650        self.logger.debug('function email2trac_workflow: ')
651       
652        req = Mock(authname=self.author, perm=MockPerm(), args={})
653        ticket_system = TicketSystem(self.env)
654       
655        try:
656            workflow = self.parameters['workflow_%s' %tkt['status'].lower()]
657
658        except KeyError:
659            ##
660            # fallback for compability (Will be deprecated)
661            # workflow can be none.
662            #
663            workflow = None
664            if tkt['status'] in ['closed']:
665                workflow = self.parameters.workflow
666
667        if workflow:
668       
669            ## There can be several workflow implementations
670            #
671            for controller in ticket_system.action_controllers:
672           
673                ## Not Needed anymore, 9 Aug 2011 BvdV, we already checked the permissions
674                #
675                #if self.parameters.ticket_permission_system:
676                #
677                #
678                #   try:
679                #     if not self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
680                #            self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
681                #            break
682                #    except AttributeError, detail:
683                #            self.logger.info('controller %s has no actions implementations' %(controller))
684                #
685               
686                fields = controller.get_ticket_changes(req, tkt, workflow)
687                for key in fields.keys():
688                    self.logger.debug('\t workflow %s %s : %s' %(controller, key, fields[key]))
689                    tkt[key] = fields[key]
690
691            return True
692
693        else:
694            return False
695
[507]696    def check_permission_participants(self, tkt, action):
697        """
698        Check if the mailer is allowed to update the ticket
699        """
[538]700        self.logger.debug('function check_permission_participants %s')
[398]701
[518]702        if tkt['reporter'].lower() in [self.author.lower(), self.email_addr.lower()]:
[507]703            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]704
[507]705            return True
[398]706
[507]707        perm = PermissionSystem(self.env)
708        if perm.check_permission(action, self.author):
709            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
[408]710
[507]711            return True
712       
713        # Is the updater in the CC?
714        try:
715            cc_list = tkt['cc'].split(',')
716            for cc in cc_list:
[518]717                if self.email_addr.lower() in cc.lower.strip():
[507]718                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]719
[507]720                    return True
[398]721
[507]722        except KeyError:
723            pass
[398]724
[507]725        return False
[474]726
[507]727    def check_permission(self, tkt, action):
728        """
729        check if the reporter has the right permission for the action:
[397]730          - TICKET_CREATE
731          - TICKET_MODIFY
[474]732          - TICKET_APPEND
733          - TICKET_CHGPROP
[397]734
[507]735        There are three models:
736            - None      : no checking at all
737            - trac      : check the permission via trac permission model
738            - email2trac: ....
739        """
[538]740        self.logger.debug("function check_permission: %s" %(action))
[397]741
[507]742        if self.parameters.ticket_permission_system in ['trac']:
[398]743
[539]744            perm = PermissionCache(self.env, self.author)
745            if perm.has_permission(action):
[507]746                return True
747            else:
748                return False
[398]749
[507]750        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
751            return (self.check_permission_participants(tkt, action))   
[398]752
[507]753        ## Default is to allow everybody ticket updates and ticket creation
754        #
755        else:
756                return True
[397]757
758
[507]759    def update_ticket_fields(self, ticket, user_dict, new=None):
760        """
761        This will update the ticket fields. It will check if the
762        given fields are known and if the right values are specified
763        It will only update the ticket field value:
764            - If the field is known
765            - If the value supplied is valid for the ticket field.
766              If not then there are two options:
767               1) Skip the value (new=None)
768               2) Set default value for field (new=1)
769        """
770        self.logger.debug("function update_ticket_fields")
[169]771
[507]772        ## Check only permission model on ticket updates
773        #
774        if not new:
775            if self.parameters.ticket_permission_system:
776                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
777                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
778                    return False
[474]779
[507]780        ## Build a system dictionary from the ticket fields
781        #  with field as index and option as value
782        #
783        sys_dict = dict()
[530]784
[507]785        for field in ticket.fields:
[530]786
[507]787            try:
788                sys_dict[field['name']] = field['options']
[169]789
[507]790            except KeyError:
[531]791                sys_dict[field['name']] = None
[507]792                pass
[169]793
[507]794        ## Check user supplied fields an compare them with the
795        #  system one's
796        #
797        for field,value in user_dict.items():
[524]798
[507]799            if self.parameters.debug:
800                s = 'user_field\t %s = %s' %(field,value)
801                self.print_unicode(s)
[169]802
[531]803            if not field in sys_dict.keys(): 
[524]804                self.logger.debug('%s is not a valid field for tickets' %(field))
805                continue
806
[507]807            ## To prevent mail loop
808            #
809            if field == 'cc':
[301]810
[507]811                cc_list = user_dict['cc'].split(',')
[301]812
[507]813                if self.trac_smtp_from in cc_list:
814                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
[408]815
[507]816                    cc_list.remove(self.trac_smtp_from)
[301]817
[507]818                value = ','.join(cc_list)
819               
[301]820
[507]821            ## Check if every value is allowed for this field
822            #
[531]823            if sys_dict[field]:
[169]824
[507]825                if value in sys_dict[field]:
826                    ticket[field] = value
827                else:
828                    ## Must we set a default if value is not allowed
829                    #
830                    if new:
831                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]832
[507]833            else:
834                ## Only set if we have a value
835                #
836                #if value:
837                #   ticket[field] = value
838                ticket[field] = value
[202]839
[507]840            if self.parameters.debug:
841                s = 'ticket_field\t %s = %s' %(field,  ticket[field])
842                self.print_unicode(s)
[345]843
[507]844    def ticket_update(self, m, id, spam):
845        """
846        If the current email is a reply to an existing ticket, this function
847        will append the contents of this email to that ticket, instead of
848        creating a new one.
849        """
850        self.logger.debug("function ticket_update")
[202]851
[507]852        if not self.parameters.ticket_update:
853            self.logger.debug("ticket_update disabled")
854            return False
[456]855
[507]856        ## Must we update ticket fields
857        #
858        update_fields = dict()
859        try:
860            id, keywords = string.split(id, '?')
[262]861
[507]862            update_fields = self.str_to_dict(keywords)
[165]863
[507]864            ## Strip '#'
865            #
866            self.id = int(id[1:])
[165]867
[507]868        except ValueError:
[390]869
[507]870            ## Strip '#'
871            #
872            self.id = int(id[1:])
[164]873
[507]874        self.logger.debug("ticket_update id %s" %id)
[71]875
[507]876        ## When is the change committed
877        #
878        if self.VERSION < 0.11:
879            when = int(time.time())
880        else:
881            when = datetime.now(util.datefmt.utc)
[77]882
[507]883        try:
884            tkt = Ticket(self.env, self.id, self.db)
[390]885
[507]886        except util.TracError, detail:
[390]887
[507]888            ## Not a valid ticket
889            #
890            self.id = None
891            return False
[126]892
[507]893        ## Check the permission of the reporter
894        #
895        if self.parameters.ticket_permission_system:
896            if not self.check_permission(tkt, 'TICKET_APPEND'):
897                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
898                return False
[398]899
[507]900        ## How many changes has this ticket
901        #
902        # cnum = len(tkt.get_changelog())
903        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
904        cnum = sum(1 for e in grouped) + 1
[288]905
[486]906
[507]907        ## reopen the ticket if it is was closed
908        #  We must use the ticket workflow framework
909        #
910        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[257]911
[545]912            if not self.email2trac_workflow(tkt):
[538]913
[507]914                if tkt['status'] in ['closed']:
[526]915                    tkt['status'] = 'reopened'
916                    tkt['resolution'] = ''
917
[507]918        ## Old pre 0.11 situation
919        #
920        elif self.parameters.email_triggers_workflow:
[470]921
[507]922            self.logger.debug('email triggers workflow pre trac 0.11')
[470]923
[507]924            if tkt['status'] in ['closed']:
925                tkt['status'] = 'reopened'
926                tkt['resolution'] = ''
[257]927
[507]928        else:
929            self.logger.debug('email triggers workflow disabled')
[469]930
[507]931        ## Must we update some ticket fields properties via subject line
932        #
933        if update_fields:
934            self.update_ticket_fields(tkt, update_fields)
[166]935
[507]936        message_parts = self.get_message_parts(m)
937        message_parts = self.unique_attachment_names(message_parts)
[210]938
[507]939        ## Must we update some ticket fields properties via body_text
940        #
941        if self.properties:
942                self.update_ticket_fields(tkt, self.properties)
[309]943
[507]944        if self.parameters.email_header:
945            message_parts.insert(0, self.email_header_txt(m))
[76]946
[507]947        body_text = self.get_body_text(message_parts)
[236]948
[507]949        error_with_attachments = self.attach_attachments(message_parts)
[348]950
[507]951        if body_text.strip() or update_fields or self.properties:
[543]952
[507]953            if self.parameters.dry_run:
954                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[543]955
[507]956            else:
957                if error_with_attachments:
958                    body_text = '%s\\%s' %(error_with_attachments, body_text)
[543]959
[507]960                self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
961                tkt.save_changes(self.author, body_text, when, None, str(cnum))
962           
963        if not spam:
964            self.notify(tkt, False, when)
[72]965
[507]966        return True
[71]967
[507]968    def set_ticket_fields(self, ticket):
969        """
970        set the ticket fields to value specified
971            - /etc/email2trac.conf with <prefix>_<field>
972            - trac default values, trac.ini
973        """
974        self.logger.debug('function set_ticket_fields')
[410]975
[507]976        user_dict = dict()
[202]977
[507]978        for field in ticket.fields:
[202]979
[507]980            name = field['name']
[202]981
[507]982            ## default trac value
983            #
984            if not field.get('custom'):
985                value = self.get_config('ticket', 'default_%s' %(name) )
986                if (name in ['resolution']) and (value in ['fixed']):
987                    value = None
988            else:
989                ##  Else get the default value for reporter
990                #
991                value = field.get('value')
992                options = field.get('options')
[345]993
[507]994                if value and options and (value not in options):
995                     value = options[int(value)]
996   
997            if self.parameters.debug:
998                s = 'trac[%s] = %s' %(name, value)
999                self.print_unicode(s)
[202]1000
[507]1001            ## email2trac.conf settings
1002            #
1003            prefix = self.parameters.ticket_prefix
1004            try:
1005                value = self.parameters['%s_%s' %(prefix, name)]
1006                if self.parameters.debug:
1007                    s = 'email2trac[%s] = %s ' %(name, value)
1008                    self.print_unicode(s)
[202]1009
[507]1010            except KeyError, detail:
1011                pass
1012       
1013            if value:
1014                user_dict[name] = value
1015                if self.parameters.debug:
1016                    s = 'used %s = %s' %(name, value)
1017                    self.print_unicode(s)
[202]1018
[507]1019        self.update_ticket_fields(ticket, user_dict, new=1)
[202]1020
[507]1021        if 'status' not in user_dict.keys():
1022            ticket['status'] = 'new'
[202]1023
1024
[507]1025    def ticket_update_by_subject(self, subject):
1026        """
1027        This list of Re: prefixes is probably incomplete. Taken from
1028        wikipedia. Here is how the subject is matched
1029          - Re: <subject>
1030          - Re: (<Mail list label>:)+ <subject>
[202]1031
[507]1032        So we must have the last column
1033        """
1034        self.logger.debug('function ticket_update_by_subject')
[356]1035
[533]1036        found_id = None
[507]1037        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1038               
1039            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1040            result = SUBJECT_RE.search(subject)
[356]1041
[507]1042            if result:
1043                ## This is a reply
1044                #
1045                orig_subject = result.group(1)
[356]1046
[507]1047                self.logger.debug('subject search string: %s' %(orig_subject))
[356]1048
[507]1049                cursor = self.db.cursor()
1050                summaries = [orig_subject, '%%: %s' % orig_subject]
[356]1051
[520]1052                ## Time resolution is in micoseconds
[507]1053                #
[520]1054                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
[356]1055
[529]1056                if self.VERSION < 0.12:
1057                    lookback = util.datefmt.to_timestamp(search_date)
1058                else:
1059                    lookback = util.datefmt.to_utimestamp(search_date)
1060
[507]1061                for summary in summaries:
1062                    self.logger.debug('Looking for summary matching: "%s"' % summary)
[408]1063
[533]1064                    sql = """SELECT id, reporter FROM ticket
[507]1065                            WHERE changetime >= %s AND summary LIKE %s
1066                            ORDER BY changetime DESC"""
[533]1067
[507]1068                    cursor.execute(sql, [lookback, summary.strip()])
[356]1069
[507]1070                    for row in cursor:
[408]1071
[533]1072                        (matched_id, sql_reporter) = row
[408]1073
[533]1074                        ## Save first entry.
1075                        #
1076                        if not found_id:
1077                            found_id = matched_id
1078                           
1079                        ## If subject and reporter are the same. The we certainly have found the right ticket
1080                        #
1081                        if sql_reporter == self.author:
1082                            self.logger.debug('Found matching reporter: %s with ticket id: %d' %(sql_reporter, matched_id))
1083                            found_id = matched_id
1084                            break
[356]1085
[533]1086                    if found_id:
1087                        self.logger.debug('Found matching ticket id: %d' % found_id)
1088                        found_id = '#%d' % found_id
1089                        return (found_id, orig_subject)
1090                   
1091                    # obesolete !??
1092                    #subject = orig_subject
[356]1093
[533]1094        return (found_id, subject)
[356]1095
1096
[507]1097    def new_ticket(self, msg, subject, spam, set_fields = None):
1098        """
1099        Create a new ticket
1100        """
1101        self.logger.debug('function new_ticket')
[250]1102
[507]1103        tkt = Ticket(self.env)
[326]1104
[507]1105        self.set_reply_fields(tkt, msg)
[326]1106
[507]1107        self.set_ticket_fields(tkt)
[202]1108
[507]1109        ## Check the permission of the reporter
1110        #
1111        if self.parameters.ticket_permission_system:
1112            if not self.check_permission(tkt, 'TICKET_CREATE'):
1113                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1114                return False
[397]1115
[507]1116        ## Old style setting for component, will be removed
1117        #
1118        if spam:
1119            tkt['component'] = 'Spam'
[204]1120
[507]1121        elif self.parameters.has_key('component'):
1122            tkt['component'] = self.parameters['component']
[201]1123
[507]1124        if not msg['Subject']:
1125            tkt['summary'] = u'(No subject)'
1126        else:
1127            tkt['summary'] = subject
[22]1128
1129
[507]1130        if set_fields:
1131            rest, keywords = string.split(set_fields, '?')
[262]1132
[507]1133            if keywords:
1134                update_fields = self.str_to_dict(keywords)
1135                self.update_ticket_fields(tkt, update_fields)
[262]1136
[296]1137
[507]1138        message_parts = self.get_message_parts(msg)
[309]1139
[507]1140        ## Must we update some ticket fields properties via body_text
1141        #
1142        if self.properties:
1143                self.update_ticket_fields(tkt, self.properties)
[309]1144
[507]1145        message_parts = self.unique_attachment_names(message_parts)
1146       
1147        ## produce e-mail like header
1148        #
1149        head = ''
1150        if self.parameters.email_header:
1151            head = self.email_header_txt(msg)
1152            message_parts.insert(0, head)
1153           
1154        body_text = self.get_body_text(message_parts)
[45]1155
[507]1156        tkt['description'] = body_text
[90]1157
[507]1158        ## When is the change committed
1159        #
1160        if self.VERSION < 0.11:
1161            when = int(time.time())
1162        else:
1163            when = datetime.now(util.datefmt.utc)
[45]1164
[507]1165        if self.parameters.dry_run:
1166            print 'DRY_RUN: tkt.insert()'
1167        else:
1168            self.id = tkt.insert()
1169   
1170        changed = False
1171        comment = ''
[77]1172
[507]1173        ## some routines in trac are dependend on ticket id
1174        #  like alternate notify template
1175        #
1176        if self.parameters.alternate_notify_template:
1177            tkt['id'] = self.id
1178            changed = True
[273]1179
[507]1180        ## Rewrite the description if we have mailto enabled
1181        #
1182        if self.parameters.mailto_link:
1183            changed = True
1184            comment = u'\nadded mailto line\n'
1185            mailto = self.html_mailto_link( m['Subject'])
[253]1186
[507]1187            tkt['description'] = u'%s\r\n%s%s\r\n' \
1188                %(head, mailto, body_text)
1189   
1190        ## Save the attachments to the ticket   
1191        #
1192        error_with_attachments =  self.attach_attachments(message_parts)
[295]1193
[507]1194        if error_with_attachments:
1195            changed = True
1196            comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1197
[545]1198        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1199            if self.email2trac_workflow(tkt):
1200                changed = True
1201
[507]1202        if changed:
1203            if self.parameters.dry_run:
1204                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1205            else:
1206                tkt.save_changes(tkt['reporter'], comment)
1207                #print tkt.get_changelog(self.db, when)
[90]1208
[507]1209        if not spam:
[545]1210            self.notify(tkt, True)
[45]1211
[260]1212
[507]1213    def attach_attachments(self, message_parts, update=False):
1214        '''
1215        save any attachments as files in the ticket's directory
1216        '''
1217        self.logger.debug('function attach_attachments()')
[342]1218
[507]1219        if self.parameters.dry_run:
1220            print "DRY_RUN: no attachments attached to tickets"
1221            return ''
[342]1222
[507]1223        count = 0
[342]1224
[507]1225        ## Get Maxium attachment size
1226        #
1227        max_size = int(self.get_config('attachment', 'max_size'))
1228        status   = None
1229       
1230        for item in message_parts:
1231            ## Skip body parts
1232            #
1233            if not isinstance(item, tuple):
1234                continue
1235               
1236            (original, filename, part) = item
[477]1237
[507]1238            ## We have to determine the size so we use this temporary solution. we must escape it
1239            #  else we get UnicodeErrors.
1240            #
1241            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
1242            text = part.get_payload(decode=1)
1243            if not text:
1244                text = '(None)'
1245            fd.write(text)
1246            fd.close()
[342]1247
[507]1248            ## get the file_size
1249            #
1250            stats = os.lstat(path)
1251            file_size = stats[ST_SIZE]
[342]1252
[507]1253            ## Check if the attachment size is allowed
1254            #
1255            if (max_size != -1) and (file_size > max_size):
1256                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1257                    %(status, original, file_size, max_size)
[342]1258
[507]1259                os.unlink(path)
1260                continue
1261            else:
1262                count = count + 1
1263                   
1264            ## Insert the attachment
1265            #
1266            fd = open(path, 'rb')
1267            if self.system == 'discussion':
1268                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1269                  % (self.id,))
1270            else:
1271                self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
1272                att = attachment.Attachment(self.env, 'ticket', self.id)
[359]1273 
[507]1274            ## This will break the ticket_update system, the body_text is vaporized
1275            #  ;-(
1276            #
1277            if not update:
1278                att.author = self.author
1279                att.description = self.email_to_unicode('Added by email2trac')
[342]1280
[507]1281            try:
1282                self.logger.debug('Insert atachment')
1283                att.insert(filename, fd, file_size)
1284            except OSError, detail:
1285                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1286                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1287
[507]1288            ## Remove the created temporary filename
1289            #
1290            fd.close()
1291            os.unlink(path)
[342]1292
[507]1293        ## return error
1294        #
1295        return status
[342]1296
[359]1297########## Fullblog functions  #################################################
[339]1298
[507]1299    def blog(self, id):
1300        """
1301        The blog create/update function
1302        """
1303        ## import the modules
1304        #
1305        from tracfullblog.core import FullBlogCore
1306        from tracfullblog.model import BlogPost, BlogComment
[260]1307
[507]1308        ## instantiate blog core
1309        #
1310        blog = FullBlogCore(self.env)
1311        req = Mock(authname='anonymous', perm=MockPerm(), args={})
[312]1312
[507]1313        if id:
[260]1314
[507]1315            ## update blog
1316            #
1317            comment = BlogComment(self.env, id)
1318            comment.author = self.author
[312]1319
[507]1320            message_parts = self.get_message_parts(m)
1321            comment.comment = self.get_body_text(message_parts)
[312]1322
[507]1323            blog.create_comment(req, comment)
[260]1324
[507]1325        else:
1326            ## create blog
1327            #
1328            import time
1329            post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
[260]1330
[507]1331            #post = BlogPost(self.env, blog._get_default_postname(self.env))
1332           
1333            post.author = self.author
1334            post.title = self.email_to_unicode(m['Subject'])
[312]1335
[507]1336            message_parts = self.get_message_parts(m)
1337            post.body = self.get_body_text(message_parts)
1338           
1339            blog.create_post(req, post, self.author, u'Created by email2trac', False)
[260]1340
1341
[359]1342########## Discussion functions  ##############################################
[342]1343
[507]1344    def discussion_topic(self, content, subject):
[342]1345
[507]1346        ## Import modules.
1347        #
1348        from tracdiscussion.api import DiscussionApi
1349        from trac.util.datefmt import to_timestamp, utc
[359]1350
[507]1351        self.logger.debug('Creating a new topic in forum:', self.id)
[359]1352
[507]1353        ## Get dissussion API component.
1354        #
1355        api = self.env[DiscussionApi]
1356        context = self._create_context(content, subject)
[359]1357
[507]1358        ## Get forum for new topic.
1359        #
1360        forum = api.get_forum(context, self.id)
[359]1361
[507]1362        if not forum:
1363            self.logger.error("ERROR: Replied forum doesn't exist")
[359]1364
[507]1365        ## Prepare topic.
1366        #
1367        topic = {'forum' : forum['id'],
1368                 'subject' : context.subject,
1369                 'time': to_timestamp(datetime.now(utc)),
1370                 'author' : self.author,
1371                 'subscribers' : [self.email_addr],
1372                 'body' : self.get_body_text(context.content_parts)}
[359]1373
[507]1374        ## Add topic to DB and commit it.
1375        #
1376        self._add_topic(api, context, topic)
1377        self.db.commit()
[359]1378
[507]1379    def discussion_topic_reply(self, content, subject):
[359]1380
[507]1381        ## Import modules.
1382        #
1383        from tracdiscussion.api import DiscussionApi
1384        from trac.util.datefmt import to_timestamp, utc
[359]1385
[507]1386        self.logger.debug('Replying to discussion topic', self.id)
[359]1387
[507]1388        ## Get dissussion API component.
1389        #
1390        api = self.env[DiscussionApi]
1391        context = self._create_context(content, subject)
[359]1392
[507]1393        ## Get replied topic.
1394        #
1395        topic = api.get_topic(context, self.id)
[359]1396
[507]1397        if not topic:
1398            self.logger.error("ERROR: Replied topic doesn't exist")
[359]1399
[507]1400        ## Prepare message.
1401        #
1402        message = {'forum' : topic['forum'],
1403                   'topic' : topic['id'],
1404                   'replyto' : -1,
1405                   'time' : to_timestamp(datetime.now(utc)),
1406                   'author' : self.author,
1407                   'body' : self.get_body_text(context.content_parts)}
[359]1408
[507]1409        ## Add message to DB and commit it.
1410        #
1411        self._add_message(api, context, message)
1412        self.db.commit()
[359]1413
[507]1414    def discussion_message_reply(self, content, subject):
[359]1415
[507]1416        ## Import modules.
1417        #
1418        from tracdiscussion.api import DiscussionApi
1419        from trac.util.datefmt import to_timestamp, utc
[359]1420
[507]1421        self.logger.debug('Replying to discussion message', self.id)
[359]1422
[507]1423        ## Get dissussion API component.
1424        #
1425        api = self.env[DiscussionApi]
1426        context = self._create_context(content, subject)
[359]1427
[507]1428        ## Get replied message.
1429        #
1430        message = api.get_message(context, self.id)
[359]1431
[507]1432        if not message:
1433            self.logger.error("ERROR: Replied message doesn't exist")
[359]1434
[507]1435        ## Prepare message.
1436        #
1437        message = {'forum' : message['forum'],
1438                   'topic' : message['topic'],
1439                   'replyto' : message['id'],
1440                   'time' : to_timestamp(datetime.now(utc)),
1441                   'author' : self.author,
1442                   'body' : self.get_body_text(context.content_parts)}
[359]1443
[507]1444        ## Add message to DB and commit it.
1445        #
1446        self._add_message(api, context, message)
1447        self.db.commit()
[359]1448
[507]1449    def _create_context(self, content, subject):
[359]1450
[507]1451        ## Import modules.
1452        #
1453        from trac.mimeview import Context
1454        from trac.web.api import Request
1455        from trac.perm import PermissionCache
[359]1456
[507]1457        ## TODO: Read server base URL from config.
1458        #  Create request object to mockup context creation.
1459        #
1460        environ = {'SERVER_PORT' : 80,
1461                   'SERVER_NAME' : 'test',
1462                   'REQUEST_METHOD' : 'POST',
1463                   'wsgi.url_scheme' : 'http',
1464                   'wsgi.input' : sys.stdin}
1465        chrome =  {'links': {},
1466                   'scripts': [],
1467                   'ctxtnav': [],
1468                   'warnings': [],
1469                   'notices': []}
[359]1470
[507]1471        if self.env.base_url_for_redirect:
1472            environ['trac.base_url'] = self.env.base_url
[359]1473
[507]1474        req = Request(environ, None)
1475        req.chrome = chrome
1476        req.tz = 'missing'
1477        req.authname = self.author
1478        req.perm = PermissionCache(self.env, self.author)
1479        req.locale = None
[359]1480
[507]1481        ## Create and return context.
1482        #
1483        context = Context.from_request(req)
1484        context.realm = 'discussion-email2trac'
1485        context.cursor = self.db.cursor()
1486        context.content = content
1487        context.subject = subject
[359]1488
[507]1489        ## Read content parts from content.
1490        #
1491        context.content_parts = self.get_message_parts(content)
1492        context.content_parts = self.unique_attachment_names(
1493          context.content_parts)
[359]1494
[507]1495        return context
[359]1496
[507]1497    def _add_topic(self, api, context, topic):
1498        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1499
[507]1500        ## Filter topic.
1501        #
1502        for discussion_filter in api.discussion_filters:
1503            accept, topic_or_error = discussion_filter.filter_topic(
1504              context, topic)
1505            if accept:
1506                topic = topic_or_error
1507            else:
1508                raise TracError(topic_or_error)
[359]1509
[507]1510        ## Add a new topic.
1511        #
1512        api.add_topic(context, topic)
[359]1513
[507]1514        ## Get inserted topic with new ID.
1515        #
1516        topic = api.get_topic_by_time(context, topic['time'])
[359]1517
[507]1518        ## Attach attachments.
1519        #
1520        self.id = topic['id']
1521        self.attach_attachments(context.content_parts, True)
[359]1522
[507]1523        ## Notify change listeners.
1524        #
1525        for listener in api.topic_change_listeners:
1526            listener.topic_created(context, topic)
[359]1527
[507]1528    def _add_message(self, api, context, message):
1529        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1530
[507]1531        ## Filter message.
1532        #
1533        for discussion_filter in api.discussion_filters:
1534            accept, message_or_error = discussion_filter.filter_message(
1535              context, message)
1536            if accept:
1537                message = message_or_error
1538            else:
1539                raise TracError(message_or_error)
[359]1540
[507]1541        ## Add message.
1542        #
1543        api.add_message(context, message)
[359]1544
[507]1545        ## Get inserted message with new ID.
1546        #
1547        message = api.get_message_by_time(context, message['time'])
[359]1548
[507]1549        ## Attach attachments.
1550        #
1551        self.id = message['topic']
1552        self.attach_attachments(context.content_parts, True)
[359]1553
[507]1554        ## Notify change listeners.
1555        #
1556        for listener in api.message_change_listeners:
1557            listener.message_created(context, message)
[359]1558
1559########## MAIN function  ######################################################
1560
[507]1561    def parse(self, fp):
1562        """
1563        """
1564        self.logger.debug('Main function parse')
1565        global m
[96]1566
[507]1567        m = email.message_from_file(fp)
1568       
1569        if not m:
1570            self.logger.debug('This is not a valid email message format')
1571            return
1572           
1573        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1574        #
1575        try:
1576            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1577        except AttributeError, detail:
1578            pass
[239]1579
[507]1580        if self.parameters.debug:     # save the entire e-mail message text
[528]1581            self.save_email_for_debug(m, self.parameters.project_name, True)
[77]1582
[507]1583        self.db = self.env.get_db_cnx()
1584        self.get_sender_info(m)
[152]1585
[507]1586        if not self.email_header_acl('white_list', self.email_addr, True):
1587            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1588            return False
[77]1589
[507]1590        if self.email_header_acl('black_list', self.email_addr, False):
1591            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1592            return False
[221]1593
[507]1594        if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1595            self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
1596            return False
[226]1597
[507]1598        ## If spam drop the message
1599        #
1600        if self.spam(m) == 'drop':
1601            return False
[194]1602
[507]1603        elif self.spam(m) == 'spam':
1604            spam_msg = True
1605        else:
1606            spam_msg = False
[204]1607
[507]1608        if not m['Subject']:
1609            subject  = 'No Subject'
1610        else:
1611            subject  = self.email_to_unicode(m['Subject'])
[304]1612
[516]1613        self.logger.info('subject: %s' %subject)
[359]1614
[507]1615        ## [hic] #1529: Re: LRZ
1616        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1617        #
1618        ticket_regex = r'''
1619            (?P<new_fields>[#][?].*)
1620            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1621            '''
1622        ## Check if  FullBlogPlugin is installed
1623        #
1624        blog_enabled = None
1625        blog_regex = ''
1626        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1627            self.logger.debug('Trac BLOG support enabled')
1628            blog_enabled = True
1629            blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1630
[77]1631
[507]1632        ## Check if DiscussionPlugin is installed
1633        #
1634        discussion_enabled = None
1635        discussion_regex = ''
1636        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1637            self.logger.debug('Trac Discussion support enabled')
1638            discussion_enabled = True
1639            discussion_regex = r'''
1640            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1641            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1642            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1643            '''
[77]1644
[359]1645
[507]1646        regex_str = ticket_regex + blog_regex + discussion_regex
1647        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
[359]1648
[507]1649        ## Find out if this is a ticket, a blog or a discussion
1650        #
1651        result =  SYSTEM_RE.search(subject)
[390]1652
[507]1653        if result:
1654            ## update ticket + fields
1655            #
1656            if result.group('reply'):
1657                self.system = 'ticket'
[260]1658
[507]1659                ## Skip the last ':' character
1660                #
1661                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1662                    self.new_ticket(m, subject, spam_msg)
[390]1663
[507]1664            ## New ticket + fields
1665            #
1666            elif result.group('new_fields'):
1667                self.system = 'ticket'
1668                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
[262]1669
[507]1670            if blog_enabled:
1671                if result.group('blog'):
1672                    self.system = 'blog'
1673                    self.blog(result.group('blog_id'))
[359]1674
[507]1675            if discussion_enabled:
1676                ## New topic.
1677                #
1678                if result.group('forum'):
1679                    self.system = 'discussion'
1680                    self.id = int(result.group('forum_id'))
1681                    self.discussion_topic(m, subject[result.end('forum'):])
[359]1682
[507]1683                ## Reply to topic.
1684                #
1685                elif result.group('topic'):
1686                    self.system = 'discussion'
1687                    self.id = int(result.group('topic_id'))
1688                    self.discussion_topic_reply(m, subject[result.end('topic'):])
[359]1689
[507]1690                ## Reply to topic message.
1691                #
1692                elif result.group('message'):
1693                    self.system = 'discussion'
1694                    self.id = int(result.group('message_id'))
1695                    self.discussion_message_reply(m, subject[result.end('message'):])
[359]1696
[507]1697        else:
1698            self.system = 'ticket'
1699            (matched_id, subject) = self.ticket_update_by_subject(subject)
1700            if matched_id:
1701                if not self.ticket_update(m, matched_id, spam_msg):
1702                    self.new_ticket(m, subject, spam_msg)
1703            else:
1704                ## No update by subject, so just create a new ticket
1705                #
1706                self.new_ticket(m, subject, spam_msg)
[353]1707
[356]1708
[343]1709########## BODY TEXT functions  ###########################################################
1710
[507]1711    def strip_signature(self, text):
1712        """
1713        Strip signature from message, inspired by Mailman software
1714        """
1715        self.logger.debug('function strip_signature')
[462]1716
[507]1717        body = []
1718        for line in text.splitlines():
1719            if line == '-- ':
1720                break
1721            body.append(line)
[136]1722
[507]1723        return ('\n'.join(body))
[136]1724
[507]1725    def reflow(self, text, delsp = 0):
1726        """
1727        Reflow the message based on the format="flowed" specification (RFC 3676)
1728        """
1729        flowedlines = []
1730        quotelevel = 0
1731        prevflowed = 0
[231]1732
[507]1733        for line in text.splitlines():
1734            from re import match
1735           
1736            ## Figure out the quote level and the content of the current line
1737            #
1738            m = match('(>*)( ?)(.*)', line)
1739            linequotelevel = len(m.group(1))
1740            line = m.group(3)
[231]1741
[507]1742            ## Determine whether this line is flowed
1743            #
1744            if line and line != '-- ' and line[-1] == ' ':
1745                flowed = 1
1746            else:
1747                flowed = 0
[231]1748
[507]1749            if flowed and delsp and line and line[-1] == ' ':
1750                line = line[:-1]
[231]1751
[507]1752            ## If the previous line is flowed, append this line to it
1753            #
1754            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1755                flowedlines[-1] += line
[477]1756
[507]1757            ## Otherwise, start a new line
1758            #
1759            else:
1760                flowedlines.append('>' * linequotelevel + line)
[231]1761
[507]1762            prevflowed = flowed
1763           
[231]1764
[507]1765        return '\n'.join(flowedlines)
[231]1766
[507]1767    def strip_quotes(self, text):
1768        """
1769        Strip quotes from message by Nicolas Mendoza
1770        """
1771        self.logger.debug('function strip_quotes')
[462]1772
[507]1773        body = []
1774        for line in text.splitlines():
1775            try:
[462]1776
[507]1777                if line.startswith(self.parameters.email_quote):
1778                    continue
[462]1779
[507]1780            except UnicodeDecodeError:
[462]1781
[507]1782                tmp_line = self.email_to_unicode(line)
1783                if tmp_line.startswith(self.parameters.email_quote):
1784                    continue
1785               
1786            body.append(line)
[151]1787
[507]1788        return ('\n'.join(body))
[191]1789
[507]1790    def inline_properties(self, text):
1791        """
1792        Parse text if we use inline keywords to set ticket fields
1793        """
1794        self.logger.debug('function inline_properties')
[309]1795
[507]1796        properties = dict()
1797        body = list()
[309]1798
[507]1799        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
[309]1800
[507]1801        for line in text.splitlines():
1802            match = INLINE_EXP.match(line)
1803            if match:
1804                keyword, value = match.groups()
[509]1805
1806                if self.parameters.inline_properties_first_wins:
1807                    if keyword in self.properties.keys():
1808                        continue
1809
[507]1810                self.properties[keyword] = value.strip()
[509]1811                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
[408]1812
[507]1813            else:
1814                body.append(line)
1815               
1816        return '\n'.join(body)
[309]1817
1818
[507]1819    def wrap_text(self, text, replace_whitespace = False):
1820        """
1821        Will break a lines longer then given length into several small
1822        lines of size given length
1823        """
1824        import textwrap
[154]1825
[507]1826        LINESEPARATOR = '\n'
1827        reformat = ''
[151]1828
[507]1829        for s in text.split(LINESEPARATOR):
1830            tmp = textwrap.fill(s, self.parameters.use_textwrap)
1831            if tmp:
1832                reformat = '%s\n%s' %(reformat,tmp)
1833            else:
1834                reformat = '%s\n' %reformat
[153]1835
[507]1836        return reformat
[153]1837
[507]1838        # Python2.4 and higher
1839        #
1840        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1841        #
[154]1842
[343]1843########## EMAIL attachements functions ###########################################################
1844
[507]1845    def inline_part(self, part):
1846        """
1847        """
1848        self.logger.debug('function inline_part()')
[154]1849
[507]1850        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
[340]1851
[507]1852    def get_message_parts(self, msg):
1853        """
1854        parses the email message and returns a list of body parts and attachments
1855        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1856        """
1857        self.logger.debug('function get_message_parts()')
[317]1858
[507]1859        message_parts = list()
1860   
1861        ALTERNATIVE_MULTIPART = False
[278]1862
[507]1863        for part in msg.walk():
1864            content_maintype = part.get_content_maintype()
1865            content_type =  part.get_content_type()
[278]1866
[507]1867            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
1868            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
[498]1869
[507]1870            ## Check content type
1871            #
1872            if content_type in self.STRIP_CONTENT_TYPES:
1873                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
1874                continue
[238]1875
[507]1876            ## Catch some mulitpart execptions
1877            #
1878            if content_type == 'multipart/alternative':
1879                ALTERNATIVE_MULTIPART = True
1880                continue
[278]1881
[507]1882            ## Skip multipart containers
1883            #
1884            if content_maintype == 'multipart':
1885                self.logger.debug("\t Skipping multipart container")
1886                continue
1887           
1888            ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1889            #
1890            inline = self.inline_part(part)
[236]1891
[507]1892            ## Drop HTML message
1893            #
1894            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
1895                if content_type == 'text/html':
1896                    self.logger.debug('\t Skipping alternative HTML message')
1897                    ALTERNATIVE_MULTIPART = False
1898                    continue
[278]1899
[498]1900
[507]1901            ## Save all non plain text message as attachment
1902            #
1903            if not content_type in ['text/plain']:
[498]1904
[507]1905                if self.parameters.debug:
1906                    s = '\t Filename: %s' % part.get_filename()
1907                    self.print_unicode(s)
[498]1908
[507]1909                ## First try to use email header function to convert filename.
1910                #  If this fails the use the plain filename
1911                #
1912                try:
1913                    filename = self.email_to_unicode(part.get_filename())
1914                except UnicodeEncodeError, detail:
1915                    filename = part.get_filename()
[498]1916
[507]1917                message_parts.append((filename, part))
[498]1918
[507]1919                ## We van only convert html messages
1920                #
1921                if not content_type == 'text/html':
1922                    self.logger.debug('\t Appending content_type = %s' %(content_type))
1923                    continue
[500]1924
[507]1925            if not inline:
1926                    self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type))
1927                    continue
1928               
1929            ## Try to decode message part. We have a html or plain text messafe
1930            #
1931            body_text = part.get_payload(decode=1)
1932            if not body_text:           
1933                body_text = part.get_payload(decode=0)
[236]1934
[507]1935            ## Try to convert html message
1936            #
1937            if content_type == 'text/html':
[502]1938
[507]1939                body_text = self.html_2_txt(body_text)
1940                if not body_text:
1941                    continue
[498]1942
[507]1943            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1944            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1945
[507]1946            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
1947                body_text = self.reflow(body_text, delsp == 'yes')
1948   
1949            if self.parameters.strip_signature:
1950                body_text = self.strip_signature(body_text)
[22]1951
[507]1952            if self.parameters.strip_quotes:
1953                body_text = self.strip_quotes(body_text)
[191]1954
[507]1955            if self.parameters.inline_properties:
1956                body_text = self.inline_properties(body_text)
[309]1957
[507]1958            if self.parameters.use_textwrap:
1959                body_text = self.wrap_text(body_text)
[148]1960
[507]1961            ## Get contents charset (iso-8859-15 if not defined in mail headers)
1962            #
1963            charset = part.get_content_charset()
1964            if not charset:
1965                charset = 'iso-8859-15'
[102]1966
[507]1967            try:
1968                ubody_text = unicode(body_text, charset)
[100]1969
[507]1970            except UnicodeError, detail:
1971                ubody_text = unicode(body_text, 'iso-8859-15')
[89]1972
[507]1973            except LookupError, detail:
1974                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1975
[507]1976            if self.parameters.verbatim_format:
1977                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1978            else:
1979                message_parts.append('%s' %ubody_text)
[22]1980
[507]1981        return message_parts
1982       
1983    def unique_attachment_names(self, message_parts):
1984        """
1985        Make sure we have unique names attachments:
1986          - check if it contains illegal characters
1987          - Rename "None" filenames to "untitled-part"
1988        """
1989        self.logger.debug('function unique_attachment_names()')
1990        renamed_parts = []
1991        attachment_names = set()
[296]1992
[507]1993        for item in message_parts:
1994           
1995            ## If not an attachment, leave it alone
1996            #
1997            if not isinstance(item, tuple):
1998                renamed_parts.append(item)
1999                continue
2000               
2001            (filename, part) = item
[295]2002
[507]2003            ## If filename = None, use a default one
2004            #
2005            if filename in [ 'None']:
2006                filename = 'untitled-part'
2007                self.logger.info('Rename filename "None" to: %s' %filename)
[22]2008
[507]2009                ## Guess the extension from the content type, use non strict mode
2010                #  some additional non-standard but commonly used MIME types
2011                #  are also recognized
2012                #
2013                ext = mimetypes.guess_extension(part.get_content_type(), False)
2014                if not ext:
2015                    ext = '.bin'
[22]2016
[507]2017                filename = '%s%s' % (filename, ext)
[22]2018
[507]2019            ## Discard relative paths for windows/unix in attachment names
2020            #
2021            #filename = filename.replace('\\', '/').replace(':', '/')
2022            filename = filename.replace('\\', '_')
2023            filename = filename.replace('/', '_')
[347]2024
[507]2025            ## remove linefeed char
2026            #
2027            for forbidden_char in ['\r', '\n']:
2028                filename = filename.replace(forbidden_char,'')
[465]2029
[507]2030            ## We try to normalize the filename to utf-8 NFC if we can.
2031            #  Files uploaded from OS X might be in NFD.
2032            #  Check python version and then try it
2033            #
2034            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2035            #   try:
2036            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2037            #   except TypeError:
2038            #       pass
[100]2039
[507]2040            ## Make the filename unique for this ticket
2041            #
2042            num = 0
2043            unique_filename = filename
2044            dummy_filename, ext = os.path.splitext(filename)
[134]2045
[507]2046            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2047                num += 1
2048                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2049               
2050            if self.parameters.debug:
2051                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2052                self.print_unicode(s)
[100]2053
[507]2054            attachment_names.add(unique_filename)
[236]2055
[507]2056            renamed_parts.append((filename, unique_filename, part))
2057   
2058        return renamed_parts
2059           
2060           
2061    def attachment_exists(self, filename):
[250]2062
[507]2063        if self.parameters.debug:
2064            s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2065            self.print_unicode(s)
[250]2066
[507]2067        ## We have no valid ticket id
2068        #
2069        if not self.id:
2070            return False
[250]2071
[507]2072        try:
2073            if self.system == 'discussion':
2074                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
2075                  % (self.id,), filename)
2076            else:
2077                att = attachment.Attachment(self.env, 'ticket', self.id,
2078                  filename)
2079            return True
2080        except attachment.ResourceNotFound:
2081            return False
[343]2082
2083########## TRAC Ticket Text ###########################################################
[507]2084           
2085    def get_body_text(self, message_parts):
2086        """
2087        """
2088        self.logger.debug('function get_body_text()')
[498]2089
[507]2090        body_text = []
2091       
2092        for part in message_parts:
2093       
2094            ## Plain text part, append it
2095            #
2096            if not isinstance(part, tuple):
2097                body_text.extend(part.strip().splitlines())
2098                body_text.append("")
2099                continue
[490]2100
[507]2101            (original, filename, part) = part
2102            inline = self.inline_part(part)
[491]2103
[507]2104            ## Skip generation of attachment link if html is converted to text
2105            #
2106            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2107                s = 'Skipping attachment link for html part: %s' %(filename)
2108                self.print_unicode(s)
2109                continue
2110           
2111            if part.get_content_maintype() == 'image' and inline:
2112                if self.system != 'discussion':
2113                    s = 'wiki image link for: %s' %(filename)
2114                    self.print_unicode(s)
2115                    body_text.append('[[Image(%s)]]' % filename)
2116                body_text.append("")
2117            else:
2118                if self.system != 'discussion':
2119                    s = 'wiki attachment link for: %s' %(filename)
2120                    self.print_unicode(s)
2121                    body_text.append('[attachment:"%s"]' % filename)
2122                body_text.append("")
[490]2123
[507]2124        ## Convert list body_texts to string
2125        #
2126        body_text = '\r\n'.join(body_text)
2127        return body_text
[236]2128
[507]2129    def html_mailto_link(self, subject):
2130        """
2131        This function returns a HTML mailto tag with the ticket id and author email address
2132        """
2133        self.logger.debug("function html_mailto_link")
2134        if not self.author:
2135            author = self.email_addr
2136        else:   
2137            author = self.author
[343]2138
[507]2139        if not self.parameters.mailto_cc:
2140            self.parameters.mailto_cc = ''
[434]2141
[507]2142        ## use urllib to escape the chars
2143        #
[517]2144        s = '%s?Subject=%s&cc=%s' %(
[507]2145               urllib.quote(self.email_addr),
2146               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2147               urllib.quote(self.parameters.mailto_cc)
2148               )
[343]2149
[507]2150        if self.VERSION in [ 0.10 ]:
2151            s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
2152        else:
2153            s = '[mailto:"%s" Reply to: %s]' %(s, author)
[506]2154
[507]2155        self.logger.debug("\tmailto link %s" %s)
2156        return s
[343]2157
2158########## TRAC notify section ###########################################################
2159
[507]2160    def notify(self, tkt, new=True, modtime=0):
2161        """
2162        A wrapper for the TRAC notify function. So we can use templates
2163        """
2164        self.logger.debug('function notify()')
[392]2165
[509]2166        if self.parameters.notify_reporter:
2167            self.logger.debug('\t Notify reporter set')
[508]2168            global sender_email
2169            sender_email = self.email_addr
2170 
2171            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2172            import trac.notification as Email2TracNotification
2173            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2174
2175        if self.parameters.dry_run  :
[507]2176                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
2177                return
2178        try:
[392]2179
[507]2180            #from trac.ticket.web_ui import TicketModule
2181            #from trac.ticket.notification import TicketNotificationSystem
2182            #ticket_sys = TicketNotificationSystem(self.env)
2183            #a = TicketModule(self.env)
2184            #print a.__dict__
2185            #tn_sys = TicketNotificationSystem(self.env)
2186            #print tn_sys
2187            #print tn_sys.__dict__
2188            #sys.exit(0)
[392]2189
[507]2190            ## create false {abs_}href properties, to trick Notify()
2191            #
[540]2192            if self.VERSION in [0.10]:
[507]2193                self.env.abs_href = Href(self.get_config('project', 'url'))
2194                self.env.href = Href(self.get_config('project', 'url'))
[22]2195
[507]2196            tn = TicketNotifyEmail(self.env)
[213]2197
[507]2198            if self.parameters.alternate_notify_template:
[222]2199
[507]2200                if self.VERSION >= 0.11:
[222]2201
[507]2202                    from trac.web.chrome import Chrome
[222]2203
[507]2204                    if  self.parameters.alternate_notify_template_update and not new:
2205                        tn.template_name = self.parameters.alternate_notify_template_update
2206                    else:
2207                        tn.template_name = self.parameters.alternate_notify_template
[222]2208
[507]2209                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2210                       
2211                else:
[222]2212
[507]2213                    tn.template_name = self.parameters.alternate_notify_template
[42]2214
[507]2215            tn.notify(tkt, new, modtime)
[41]2216
[507]2217        except Exception, e:
2218            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2219
[508]2220########## END Class Definition  ########################################################
[22]2221
[508]2222########## Global Notificaition Function ################################################
2223def AlwaysNotifyReporter(self, resid):
2224    """
2225    Copy of def notify() to manipulate recipents to always include reporter for the
2226    notification.
2227    """
2228    (torcpts, ccrcpts) = self.get_recipients(resid)
[74]2229
[508]2230    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2231        ## additionally append sender (regardeless of settings in trac.ini)
2232        #
2233        if not sender_email in torcpts:
2234            torcpts.append(sender_email)
2235
2236    self.begin_send()
2237    self.send(torcpts, ccrcpts)
2238    self.finish_send()
2239
[343]2240########## Parse Config File  ###########################################################
[22]2241
2242def ReadConfig(file, name):
[507]2243    """
2244    Parse the config file
2245    """
2246    if not os.path.isfile(file):
2247        print 'File %s does not exist' %file
2248        sys.exit(1)
[22]2249
[507]2250    config = trac_config.Configuration(file)
2251   
2252    parentdir = config.get('DEFAULT', 'parentdir')
2253    sections = config.sections()
[22]2254
[507]2255    ## use some trac internals to get the defaults
2256    #
2257    tmp = config.parser.defaults()
2258    project =  SaraDict()
[485]2259
[507]2260    for option, value in tmp.items():
2261        try:
2262            project[option] = int(value)
2263        except ValueError:
2264            project[option] = value
[485]2265
[507]2266    if name:
2267        if name in sections:
2268            project =  SaraDict()
2269            for option, value in  config.options(name):
2270                try:
2271                    project[option] = int(value)
2272                except ValueError:
2273                    project[option] = value
[485]2274
[507]2275        elif not parentdir:
2276            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2277            print "or set parentdir in the [DEFAULT] section"
2278            sys.exit(1)
[22]2279
[507]2280    ## If parentdir then set project dir to parentdir + name
2281    #
2282    if not project.has_key('project'):
2283        if not parentdir:
2284            print "You must set project or parentdir in your configuration file"
2285            sys.exit(1)
2286        elif not name:
2287            print "You must configure a  project section in your configuration file"
2288        else:
2289            project['project'] = os.path.join(parentdir, name)
[22]2290
[528]2291    ##
2292    # Save the project name
2293    #
2294    project['project_name'] = os.path.basename(project['project'])
2295
[507]2296    return project
[22]2297
[410]2298########## Setup Logging ###############################################################
[87]2299
[419]2300def setup_log(parameters, project_name, interactive=None):
[507]2301    """
[516]2302    Setup logging
[410]2303
[507]2304    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
[420]2305    would be interpreted by the ConfigParser itself.
[507]2306    """
2307    logger = logging.getLogger('email2trac %s' %project_name)
[410]2308
[507]2309    if interactive:
2310        parameters.log_type = 'stderr'
[414]2311
[507]2312    if not parameters.log_type:
2313        if sys.platform in ['win32', 'cygwin']:
[521]2314            parameters.log_type = 'eventlog'
[507]2315        else:
2316            parameters.log_type = 'syslog'
[410]2317
[507]2318    if parameters.log_type == 'file':
[415]2319
[507]2320        if not parameters.log_file:
2321            parameters.log_file = 'email2trac.log'
[415]2322
[507]2323        if not os.path.isabs(parameters.log_file):
2324            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
[410]2325
[507]2326        log_handler = logging.FileHandler(parameters.log_file)
[415]2327
[507]2328    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2329        ## Requires win32 extensions
2330        #
[532]2331        logid = "email2trac"
[507]2332        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
[410]2333
[507]2334    elif parameters.log_type in ('syslog', 'unix'):
2335        log_handler = logging.handlers.SysLogHandler('/dev/log')
[410]2336
[507]2337    elif parameters.log_type in ('stderr'):
2338        log_handler = logging.StreamHandler(sys.stderr)
[410]2339
[507]2340    else:
2341        log_handler = logging.handlers.BufferingHandler(0)
[410]2342
[507]2343    if parameters.log_format:
2344        parameters.log_format = parameters.log_format.replace('$(', '%(')
2345    else:
2346        parameters.log_format = '%(name)s: %(message)s'
[516]2347        if parameters.log_type in ('file', 'stderr'):
2348            parameters.log_format = '%(asctime)s ' + parameters.log_format
[410]2349
[507]2350    log_formatter = logging.Formatter(parameters.log_format)
2351    log_handler.setFormatter(log_formatter)
2352    logger.addHandler(log_handler)
[410]2353
[507]2354    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2355        logger.setLevel(logging.DEBUG)
2356        parameters.debug = 1
[410]2357
[507]2358    elif parameters.log_level in ['INFO'] or parameters.verbose:
2359        logger.setLevel(logging.INFO)
[410]2360
[507]2361    elif parameters.log_level in ['WARNING']:
2362        logger.setLevel(logging.WARNING)
[410]2363
[507]2364    elif parameters.log_level in ['ERROR']:
2365        logger.setLevel(logging.ERROR)
[410]2366
[507]2367    elif parameters.log_level in ['CRITICAL']:
2368        logger.setLevel(logging.CRITICAL)
[410]2369
[507]2370    else:
2371        logger.setLevel(logging.INFO)
[410]2372
[507]2373    return logger
[410]2374
2375
[22]2376if __name__ == '__main__':
[507]2377    ## Default config file
2378    #
2379    configfile = '@email2trac_conf@'
2380    project = ''
2381    component = ''
2382    ticket_prefix = 'default'
2383    dry_run = None
2384    verbose = None
2385    debug_interactive = None
[202]2386
[507]2387    SHORT_OPT = 'cdhf:np:t:v'
2388    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2389
[507]2390    try:
2391        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2392    except getopt.error,detail:
2393        print __doc__
2394        print detail
2395        sys.exit(1)
2396   
2397    project_name = None
2398    for opt,value in opts:
2399        if opt in [ '-h', '--help']:
2400            print __doc__
2401            sys.exit(0)
2402        elif opt in ['-c', '--component']:
2403            component = value
2404        elif opt in ['-d', '--debug']:
2405            debug_interactive = 1
2406        elif opt in ['-f', '--file']:
2407            configfile = value
2408        elif opt in ['-n', '--dry-run']:
2409            dry_run = True
2410        elif opt in ['-p', '--project']:
2411            project_name = value
2412        elif opt in ['-t', '--ticket_prefix']:
2413            ticket_prefix = value
2414        elif opt in ['-v', '--verbose']:
2415            verbose = True
2416   
2417    settings = ReadConfig(configfile, project_name)
[410]2418
[507]2419    ## The default prefix for ticket values in email2trac.conf
2420    #
2421    settings.ticket_prefix = ticket_prefix
2422    settings.dry_run = dry_run
2423    settings.verbose = verbose
[410]2424
[507]2425    if not settings.debug and debug_interactive:
2426        settings.debug = debug_interactive
[412]2427
[507]2428    if not settings.project:
2429        print __doc__
2430        print 'No Trac project is defined in the email2trac config file.'
2431        sys.exit(1)
[419]2432
[507]2433    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2434   
2435    if component:
2436        settings['component'] = component
[202]2437
[507]2438    ## Determine major trac version used to be in email2trac.conf
2439    # Quick hack for 0.12
2440    #
2441    version = '0.%s' %(trac_version.split('.')[1])
2442    if version.startswith('0.12'):
2443        version = '0.12'
[541]2444    elif version.startswith('0.13'):
2445        version = '0.13'
[373]2446
[507]2447    logger.debug("Found trac version: %s" %(version))
2448   
2449    try:
2450        if version == '0.10':
2451            from trac import attachment
2452            from trac.env import Environment
2453            from trac.ticket import Ticket
2454            from trac.web.href import Href
2455            from trac import util
2456            from trac.ticket.web_ui import TicketModule
[486]2457
[507]2458            #
2459            # return  util.text.to_unicode(str)
2460            #
2461            # see http://projects.edgewall.com/trac/changeset/2799
2462            from trac.ticket.notification import TicketNotifyEmail
2463            from trac import config as trac_config
2464            from trac.core import TracError
[359]2465
[507]2466        elif version == '0.11':
2467            from trac import attachment
2468            from trac.env import Environment
2469            from trac.ticket import Ticket
2470            from trac.web.href import Href
2471            from trac import config as trac_config
2472            from trac import util
2473            from trac.core import TracError
2474            from trac.perm import PermissionSystem
[539]2475            from trac.perm import PermissionCache
[507]2476            from trac.ticket.web_ui import TicketModule
[538]2477            from trac.ticket.api import TicketSystem
2478            from trac.test import Mock, MockPerm
[260]2479
[507]2480            #
2481            # return  util.text.to_unicode(str)
2482            #
2483            # see http://projects.edgewall.com/trac/changeset/2799
2484            from trac.ticket.notification import TicketNotifyEmail
[260]2485
[507]2486        elif version == '0.12':
2487            from trac import attachment
2488            from trac.env import Environment
2489            from trac.ticket import Ticket
2490            from trac.web.href import Href
2491            from trac import config as trac_config
2492            from trac import util
2493            from trac.core import TracError
2494            from trac.perm import PermissionSystem
[539]2495            from trac.perm import PermissionCache
[507]2496            from trac.ticket.web_ui import TicketModule
[538]2497            from trac.ticket.api import TicketSystem
2498            from trac.test import Mock, MockPerm
[368]2499
[507]2500            #
2501            # return  util.text.to_unicode(str)
2502            #
2503            # see http://projects.edgewall.com/trac/changeset/2799
2504            from trac.ticket.notification import TicketNotifyEmail
[368]2505
[541]2506        elif version == '0.13':
2507            from trac import attachment
2508            from trac.env import Environment
2509            from trac.ticket import Ticket
2510            from trac.web.href import Href
2511            from trac import config as trac_config
2512            from trac import util
2513            from trac.core import TracError
2514            from trac.perm import PermissionSystem
2515            from trac.perm import PermissionCache
2516            from trac.ticket.web_ui import TicketModule
2517            from trac.ticket.api import TicketSystem
2518            from trac.test import Mock, MockPerm
[368]2519
[541]2520            #
2521            # return  util.text.to_unicode(str)
2522            #
2523            # see http://projects.edgewall.com/trac/changeset/2799
2524            from trac.ticket.notification import TicketNotifyEmail
2525
2526
[507]2527        else:
2528            logger.error('TRAC version %s is not supported' %version)
[519]2529            sys.exit(0)
[182]2530
[507]2531        ## Must be set before environment is created
2532        #
2533        if settings.has_key('python_egg_cache'):
2534            python_egg_cache = str(settings['python_egg_cache'])
2535            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
[291]2536
[507]2537        if settings.debug > 0:
2538            logger.debug('Loading environment %s', settings.project)
[359]2539
[507]2540        try:
2541            env = Environment(settings['project'], create=0)
2542        except IOError, detail:
[534]2543            logger.error("trac error: %s" %detail)
[519]2544            sys.exit(0)
[333]2545
[507]2546        tktparser = TicketEmailParser(env, settings, logger, float(version))
2547        tktparser.parse(sys.stdin)
[22]2548
[507]2549    ## Catch all errors and use the logging module
2550    #
2551    except Exception, error:
[187]2552
[507]2553        etype, evalue, etb = sys.exc_info()
2554        for e in traceback.format_exception(etype, evalue, etb):
2555            logger.critical(e)
[187]2556
[507]2557        if m:
[528]2558            tktparser.save_email_for_debug(m, settings.project_name, True)
[97]2559
[507]2560        sys.exit(1)
[22]2561# EOB
Note: See TracBrowser for help on using the repository browser.