source: trunk/email2trac.py.in @ 530

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

updated fix for ticket update fields

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 79.9 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 530 2011-06-30 09:46:55Z 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
[507]647    def check_permission_participants(self, tkt, action):
648        """
649        Check if the mailer is allowed to update the ticket
650        """
651        self.logger.debug('function check_permission_participants')
[398]652
[518]653        if tkt['reporter'].lower() in [self.author.lower(), self.email_addr.lower()]:
[507]654            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]655
[507]656            return True
[398]657
[507]658        perm = PermissionSystem(self.env)
659        if perm.check_permission(action, self.author):
660            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
[408]661
[507]662            return True
663       
664        # Is the updater in the CC?
665        try:
666            cc_list = tkt['cc'].split(',')
667            for cc in cc_list:
[518]668                if self.email_addr.lower() in cc.lower.strip():
[507]669                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]670
[507]671                    return True
[398]672
[507]673        except KeyError:
674            pass
[398]675
[507]676        return False
[474]677
[507]678    def check_permission(self, tkt, action):
679        """
680        check if the reporter has the right permission for the action:
[397]681          - TICKET_CREATE
682          - TICKET_MODIFY
[474]683          - TICKET_APPEND
684          - TICKET_CHGPROP
[397]685
[507]686        There are three models:
687            - None      : no checking at all
688            - trac      : check the permission via trac permission model
689            - email2trac: ....
690        """
691        self.logger.debug("function check_permission")
[397]692
[507]693        if self.parameters.ticket_permission_system in ['trac']:
[398]694
[507]695            perm = PermissionSystem(self.env)
696            if perm.check_permission(action, self.author):
697                return True
698            else:
699                return False
[398]700
[507]701        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
702            return (self.check_permission_participants(tkt, action))   
[398]703
[507]704        ## Default is to allow everybody ticket updates and ticket creation
705        #
706        else:
707                return True
[397]708
709
[507]710    def update_ticket_fields(self, ticket, user_dict, new=None):
711        """
712        This will update the ticket fields. It will check if the
713        given fields are known and if the right values are specified
714        It will only update the ticket field value:
715            - If the field is known
716            - If the value supplied is valid for the ticket field.
717              If not then there are two options:
718               1) Skip the value (new=None)
719               2) Set default value for field (new=1)
720        """
721        self.logger.debug("function update_ticket_fields")
[169]722
[507]723        ## Check only permission model on ticket updates
724        #
725        if not new:
726            if self.parameters.ticket_permission_system:
727                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
728                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
729                    return False
[474]730
[507]731        ## Build a system dictionary from the ticket fields
732        #  with field as index and option as value
733        #
734        sys_dict = dict()
[530]735        sys_field_list = list()
736
[507]737        for field in ticket.fields:
[530]738
739            sys_field_list.append(field['name'])
740
[507]741            try:
742                sys_dict[field['name']] = field['options']
[169]743
[507]744            except KeyError:
745                #sys_dict[field['name']] = None
746                pass
[169]747
[507]748        ## Check user supplied fields an compare them with the
749        #  system one's
750        #
751        for field,value in user_dict.items():
[524]752
[507]753            if self.parameters.debug:
754                s = 'user_field\t %s = %s' %(field,value)
755                self.print_unicode(s)
[169]756
[530]757            if not field in sys_field_list: 
[524]758                self.logger.debug('%s is not a valid field for tickets' %(field))
759                continue
760
[507]761            ## To prevent mail loop
762            #
763            if field == 'cc':
[301]764
[507]765                cc_list = user_dict['cc'].split(',')
[301]766
[507]767                if self.trac_smtp_from in cc_list:
768                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
[408]769
[507]770                    cc_list.remove(self.trac_smtp_from)
[301]771
[507]772                value = ','.join(cc_list)
773               
[301]774
[507]775            ## Check if every value is allowed for this field
776            #
777            if sys_dict.has_key(field):
[169]778
[507]779                if value in sys_dict[field]:
780                    ticket[field] = value
781                else:
782                    ## Must we set a default if value is not allowed
783                    #
784                    if new:
785                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]786
[507]787            else:
788                ## Only set if we have a value
789                #
790                #if value:
791                #   ticket[field] = value
792                ticket[field] = value
[202]793
[507]794            if self.parameters.debug:
795                s = 'ticket_field\t %s = %s' %(field,  ticket[field])
796                self.print_unicode(s)
[345]797
[507]798    def ticket_update(self, m, id, spam):
799        """
800        If the current email is a reply to an existing ticket, this function
801        will append the contents of this email to that ticket, instead of
802        creating a new one.
803        """
804        self.logger.debug("function ticket_update")
[202]805
[507]806        if not self.parameters.ticket_update:
807            self.logger.debug("ticket_update disabled")
808            return False
[456]809
[507]810        ## Must we update ticket fields
811        #
812        update_fields = dict()
813        try:
814            id, keywords = string.split(id, '?')
[262]815
[507]816            update_fields = self.str_to_dict(keywords)
[165]817
[507]818            ## Strip '#'
819            #
820            self.id = int(id[1:])
[165]821
[507]822        except ValueError:
[390]823
[507]824            ## Strip '#'
825            #
826            self.id = int(id[1:])
[164]827
[507]828        self.logger.debug("ticket_update id %s" %id)
[71]829
[507]830        ## When is the change committed
831        #
832        if self.VERSION < 0.11:
833            when = int(time.time())
834        else:
835            when = datetime.now(util.datefmt.utc)
[77]836
[507]837        try:
838            tkt = Ticket(self.env, self.id, self.db)
[390]839
[507]840        except util.TracError, detail:
[390]841
[507]842            ## Not a valid ticket
843            #
844            self.id = None
845            return False
[126]846
[507]847        ## Check the permission of the reporter
848        #
849        if self.parameters.ticket_permission_system:
850            if not self.check_permission(tkt, 'TICKET_APPEND'):
851                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
852                return False
[398]853
[507]854        ## How many changes has this ticket
855        #
856        # cnum = len(tkt.get_changelog())
857        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
858        cnum = sum(1 for e in grouped) + 1
[288]859
[486]860
[507]861        ## reopen the ticket if it is was closed
862        #  We must use the ticket workflow framework
863        #
864        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[220]865
[507]866            self.logger.debug('Workflow ticket update fields: ')
[257]867
[507]868            from trac.ticket.default_workflow import ConfigurableTicketWorkflow
869            from trac.test import Mock, MockPerm
[468]870
[507]871            req = Mock(authname=self.author, perm=MockPerm(), args={})
872            try:
873                workflow = self.parameters['workflow_%s' %tkt['status']]
874            except KeyError:
[526]875                ##
876                # fallback for compability (Will be deprecated)
877                # workflow can be none.
[507]878                #
[527]879                workflow = None
[507]880                if tkt['status'] in ['closed']:
881                    workflow = self.parameters.workflow
[468]882
[507]883            controller = ConfigurableTicketWorkflow(self.env)
884            #print controller.actions
885            #print controller.actions.keys()
886            #print controller.get_ticket_actions(req, tkt)
887            #print controller.actions[workflow]
888            #print controller.actions[workflow]['permissions'] is a list
[469]889
[507]890            if workflow:
[257]891
[507]892                if self.parameters.ticket_permission_system:
[257]893
[507]894                    if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
895                        fields = controller.get_ticket_changes(req, tkt, workflow)
896                    else:
897                        fields = dict()
898                        self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
[257]899
[507]900                else:
901                    fields = controller.get_ticket_changes(req, tkt, workflow)
[257]902
[507]903                for key in fields.keys():
904                    self.logger.debug('\t %s : %s' %(key, fields[key]))
905                    tkt[key] = fields[key]
[257]906
[526]907            else:
908
909                if tkt['status'] in ['closed']:
910                    tkt['status'] = 'reopened'
911                    tkt['resolution'] = ''
912
[507]913        ## Old pre 0.11 situation
914        #
915        elif self.parameters.email_triggers_workflow:
[470]916
[507]917            self.logger.debug('email triggers workflow pre trac 0.11')
[470]918
[507]919            if tkt['status'] in ['closed']:
920                tkt['status'] = 'reopened'
921                tkt['resolution'] = ''
[257]922
[507]923        else:
924            self.logger.debug('email triggers workflow disabled')
[469]925
[507]926        ## Must we update some ticket fields properties via subject line
927        #
928        if update_fields:
929            self.update_ticket_fields(tkt, update_fields)
[166]930
[507]931        message_parts = self.get_message_parts(m)
932        message_parts = self.unique_attachment_names(message_parts)
[210]933
[507]934        ## Must we update some ticket fields properties via body_text
935        #
936        if self.properties:
937                self.update_ticket_fields(tkt, self.properties)
[309]938
[507]939        if self.parameters.email_header:
940            message_parts.insert(0, self.email_header_txt(m))
[76]941
[507]942        body_text = self.get_body_text(message_parts)
[236]943
[507]944        error_with_attachments = self.attach_attachments(message_parts)
[348]945
[507]946        if body_text.strip() or update_fields or self.properties:
947            if self.parameters.dry_run:
948                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
949            else:
950                if error_with_attachments:
951                    body_text = '%s\\%s' %(error_with_attachments, body_text)
952                self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
953                tkt.save_changes(self.author, body_text, when, None, str(cnum))
954           
[219]955
[507]956        if not spam:
957            self.notify(tkt, False, when)
[72]958
[507]959        return True
[71]960
[507]961    def set_ticket_fields(self, ticket):
962        """
963        set the ticket fields to value specified
964            - /etc/email2trac.conf with <prefix>_<field>
965            - trac default values, trac.ini
966        """
967        self.logger.debug('function set_ticket_fields')
[410]968
[507]969        user_dict = dict()
[202]970
[507]971        for field in ticket.fields:
[202]972
[507]973            name = field['name']
[202]974
[507]975            ## default trac value
976            #
977            if not field.get('custom'):
978                value = self.get_config('ticket', 'default_%s' %(name) )
979                if (name in ['resolution']) and (value in ['fixed']):
980                    value = None
981            else:
982                ##  Else get the default value for reporter
983                #
984                value = field.get('value')
985                options = field.get('options')
[345]986
[507]987                if value and options and (value not in options):
988                     value = options[int(value)]
989   
990            if self.parameters.debug:
991                s = 'trac[%s] = %s' %(name, value)
992                self.print_unicode(s)
[202]993
[507]994            ## email2trac.conf settings
995            #
996            prefix = self.parameters.ticket_prefix
997            try:
998                value = self.parameters['%s_%s' %(prefix, name)]
999                if self.parameters.debug:
1000                    s = 'email2trac[%s] = %s ' %(name, value)
1001                    self.print_unicode(s)
[202]1002
[507]1003            except KeyError, detail:
1004                pass
1005       
1006            if value:
1007                user_dict[name] = value
1008                if self.parameters.debug:
1009                    s = 'used %s = %s' %(name, value)
1010                    self.print_unicode(s)
[202]1011
[507]1012        self.update_ticket_fields(ticket, user_dict, new=1)
[202]1013
[507]1014        if 'status' not in user_dict.keys():
1015            ticket['status'] = 'new'
[202]1016
1017
[507]1018    def ticket_update_by_subject(self, subject):
1019        """
1020        This list of Re: prefixes is probably incomplete. Taken from
1021        wikipedia. Here is how the subject is matched
1022          - Re: <subject>
1023          - Re: (<Mail list label>:)+ <subject>
[202]1024
[507]1025        So we must have the last column
1026        """
1027        self.logger.debug('function ticket_update_by_subject')
[356]1028
[507]1029        matched_id = None
1030        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1031               
1032            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1033            result = SUBJECT_RE.search(subject)
[356]1034
[507]1035            if result:
1036                ## This is a reply
1037                #
1038                orig_subject = result.group(1)
[356]1039
[507]1040                self.logger.debug('subject search string: %s' %(orig_subject))
[356]1041
[507]1042                cursor = self.db.cursor()
1043                summaries = [orig_subject, '%%: %s' % orig_subject]
[356]1044
[520]1045                ## Time resolution is in micoseconds
[507]1046                #
[520]1047                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
[356]1048
[529]1049                if self.VERSION < 0.12:
1050                    lookback = util.datefmt.to_timestamp(search_date)
1051                else:
1052                    lookback = util.datefmt.to_utimestamp(search_date)
1053
[507]1054                for summary in summaries:
1055                    self.logger.debug('Looking for summary matching: "%s"' % summary)
[408]1056
[507]1057                    sql = """SELECT id FROM ticket
1058                            WHERE changetime >= %s AND summary LIKE %s
1059                            ORDER BY changetime DESC"""
1060                    cursor.execute(sql, [lookback, summary.strip()])
[356]1061
[507]1062                    for row in cursor:
1063                        (matched_id,) = row
[408]1064
[507]1065                        self.logger.debug('Found matching ticket id: %d' % matched_id)
[408]1066
[507]1067                        break
[356]1068
[507]1069                    if matched_id:
1070                        matched_id = '#%d' % matched_id
1071                        return (matched_id, orig_subject)
1072                   
1073                    subject = orig_subject
[356]1074
[507]1075        return (matched_id, subject)
[356]1076
1077
[507]1078    def new_ticket(self, msg, subject, spam, set_fields = None):
1079        """
1080        Create a new ticket
1081        """
1082        self.logger.debug('function new_ticket')
[250]1083
[507]1084        tkt = Ticket(self.env)
[326]1085
[507]1086        self.set_reply_fields(tkt, msg)
[326]1087
[507]1088        self.set_ticket_fields(tkt)
[202]1089
[507]1090        ## Check the permission of the reporter
1091        #
1092        if self.parameters.ticket_permission_system:
1093            if not self.check_permission(tkt, 'TICKET_CREATE'):
1094                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1095                return False
[397]1096
[507]1097        ## Old style setting for component, will be removed
1098        #
1099        if spam:
1100            tkt['component'] = 'Spam'
[204]1101
[507]1102        elif self.parameters.has_key('component'):
1103            tkt['component'] = self.parameters['component']
[201]1104
[507]1105        if not msg['Subject']:
1106            tkt['summary'] = u'(No subject)'
1107        else:
1108            tkt['summary'] = subject
[22]1109
1110
[507]1111        if set_fields:
1112            rest, keywords = string.split(set_fields, '?')
[262]1113
[507]1114            if keywords:
1115                update_fields = self.str_to_dict(keywords)
1116                self.update_ticket_fields(tkt, update_fields)
[262]1117
[296]1118
[507]1119        message_parts = self.get_message_parts(msg)
[309]1120
[507]1121        ## Must we update some ticket fields properties via body_text
1122        #
1123        if self.properties:
1124                self.update_ticket_fields(tkt, self.properties)
[309]1125
[507]1126        message_parts = self.unique_attachment_names(message_parts)
1127       
1128        ## produce e-mail like header
1129        #
1130        head = ''
1131        if self.parameters.email_header:
1132            head = self.email_header_txt(msg)
1133            message_parts.insert(0, head)
1134           
1135        body_text = self.get_body_text(message_parts)
[45]1136
[507]1137        tkt['description'] = body_text
[90]1138
[507]1139        ## When is the change committed
1140        #
1141        if self.VERSION < 0.11:
1142            when = int(time.time())
1143        else:
1144            when = datetime.now(util.datefmt.utc)
[45]1145
[507]1146        if self.parameters.dry_run:
1147            print 'DRY_RUN: tkt.insert()'
1148        else:
1149            self.id = tkt.insert()
1150   
1151        changed = False
1152        comment = ''
[77]1153
[507]1154        ## some routines in trac are dependend on ticket id
1155        #  like alternate notify template
1156        #
1157        if self.parameters.alternate_notify_template:
1158            tkt['id'] = self.id
1159            changed = True
[273]1160
[507]1161        ## Rewrite the description if we have mailto enabled
1162        #
1163        if self.parameters.mailto_link:
1164            changed = True
1165            comment = u'\nadded mailto line\n'
1166            mailto = self.html_mailto_link( m['Subject'])
[253]1167
[507]1168            tkt['description'] = u'%s\r\n%s%s\r\n' \
1169                %(head, mailto, body_text)
1170   
1171        ## Save the attachments to the ticket   
1172        #
1173        error_with_attachments =  self.attach_attachments(message_parts)
[295]1174
[507]1175        if error_with_attachments:
1176            changed = True
1177            comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1178
[507]1179        if changed:
1180            if self.parameters.dry_run:
1181                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1182            else:
1183                tkt.save_changes(tkt['reporter'], comment)
1184                #print tkt.get_changelog(self.db, when)
[90]1185
[507]1186        if not spam:
1187            self.notify(tkt, True)
[45]1188
[260]1189
[507]1190    def attach_attachments(self, message_parts, update=False):
1191        '''
1192        save any attachments as files in the ticket's directory
1193        '''
1194        self.logger.debug('function attach_attachments()')
[342]1195
[507]1196        if self.parameters.dry_run:
1197            print "DRY_RUN: no attachments attached to tickets"
1198            return ''
[342]1199
[507]1200        count = 0
[342]1201
[507]1202        ## Get Maxium attachment size
1203        #
1204        max_size = int(self.get_config('attachment', 'max_size'))
1205        status   = None
1206       
1207        for item in message_parts:
1208            ## Skip body parts
1209            #
1210            if not isinstance(item, tuple):
1211                continue
1212               
1213            (original, filename, part) = item
[477]1214
[507]1215            ## We have to determine the size so we use this temporary solution. we must escape it
1216            #  else we get UnicodeErrors.
1217            #
1218            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
1219            text = part.get_payload(decode=1)
1220            if not text:
1221                text = '(None)'
1222            fd.write(text)
1223            fd.close()
[342]1224
[507]1225            ## get the file_size
1226            #
1227            stats = os.lstat(path)
1228            file_size = stats[ST_SIZE]
[342]1229
[507]1230            ## Check if the attachment size is allowed
1231            #
1232            if (max_size != -1) and (file_size > max_size):
1233                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1234                    %(status, original, file_size, max_size)
[342]1235
[507]1236                os.unlink(path)
1237                continue
1238            else:
1239                count = count + 1
1240                   
1241            ## Insert the attachment
1242            #
1243            fd = open(path, 'rb')
1244            if self.system == 'discussion':
1245                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1246                  % (self.id,))
1247            else:
1248                self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
1249                att = attachment.Attachment(self.env, 'ticket', self.id)
[359]1250 
[507]1251            ## This will break the ticket_update system, the body_text is vaporized
1252            #  ;-(
1253            #
1254            if not update:
1255                att.author = self.author
1256                att.description = self.email_to_unicode('Added by email2trac')
[342]1257
[507]1258            try:
1259                self.logger.debug('Insert atachment')
1260                att.insert(filename, fd, file_size)
1261            except OSError, detail:
1262                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1263                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1264
[507]1265            ## Remove the created temporary filename
1266            #
1267            fd.close()
1268            os.unlink(path)
[342]1269
[507]1270        ## return error
1271        #
1272        return status
[342]1273
[359]1274########## Fullblog functions  #################################################
[339]1275
[507]1276    def blog(self, id):
1277        """
1278        The blog create/update function
1279        """
1280        ## import the modules
1281        #
1282        from tracfullblog.core import FullBlogCore
1283        from tracfullblog.model import BlogPost, BlogComment
1284        from trac.test import Mock, MockPerm
[260]1285
[507]1286        ## instantiate blog core
1287        #
1288        blog = FullBlogCore(self.env)
1289        req = Mock(authname='anonymous', perm=MockPerm(), args={})
[312]1290
[507]1291        if id:
[260]1292
[507]1293            ## update blog
1294            #
1295            comment = BlogComment(self.env, id)
1296            comment.author = self.author
[312]1297
[507]1298            message_parts = self.get_message_parts(m)
1299            comment.comment = self.get_body_text(message_parts)
[312]1300
[507]1301            blog.create_comment(req, comment)
[260]1302
[507]1303        else:
1304            ## create blog
1305            #
1306            import time
1307            post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
[260]1308
[507]1309            #post = BlogPost(self.env, blog._get_default_postname(self.env))
1310           
1311            post.author = self.author
1312            post.title = self.email_to_unicode(m['Subject'])
[312]1313
[507]1314            message_parts = self.get_message_parts(m)
1315            post.body = self.get_body_text(message_parts)
1316           
1317            blog.create_post(req, post, self.author, u'Created by email2trac', False)
[260]1318
1319
[359]1320########## Discussion functions  ##############################################
[342]1321
[507]1322    def discussion_topic(self, content, subject):
[342]1323
[507]1324        ## Import modules.
1325        #
1326        from tracdiscussion.api import DiscussionApi
1327        from trac.util.datefmt import to_timestamp, utc
[359]1328
[507]1329        self.logger.debug('Creating a new topic in forum:', self.id)
[359]1330
[507]1331        ## Get dissussion API component.
1332        #
1333        api = self.env[DiscussionApi]
1334        context = self._create_context(content, subject)
[359]1335
[507]1336        ## Get forum for new topic.
1337        #
1338        forum = api.get_forum(context, self.id)
[359]1339
[507]1340        if not forum:
1341            self.logger.error("ERROR: Replied forum doesn't exist")
[359]1342
[507]1343        ## Prepare topic.
1344        #
1345        topic = {'forum' : forum['id'],
1346                 'subject' : context.subject,
1347                 'time': to_timestamp(datetime.now(utc)),
1348                 'author' : self.author,
1349                 'subscribers' : [self.email_addr],
1350                 'body' : self.get_body_text(context.content_parts)}
[359]1351
[507]1352        ## Add topic to DB and commit it.
1353        #
1354        self._add_topic(api, context, topic)
1355        self.db.commit()
[359]1356
[507]1357    def discussion_topic_reply(self, content, subject):
[359]1358
[507]1359        ## Import modules.
1360        #
1361        from tracdiscussion.api import DiscussionApi
1362        from trac.util.datefmt import to_timestamp, utc
[359]1363
[507]1364        self.logger.debug('Replying to discussion topic', self.id)
[359]1365
[507]1366        ## Get dissussion API component.
1367        #
1368        api = self.env[DiscussionApi]
1369        context = self._create_context(content, subject)
[359]1370
[507]1371        ## Get replied topic.
1372        #
1373        topic = api.get_topic(context, self.id)
[359]1374
[507]1375        if not topic:
1376            self.logger.error("ERROR: Replied topic doesn't exist")
[359]1377
[507]1378        ## Prepare message.
1379        #
1380        message = {'forum' : topic['forum'],
1381                   'topic' : topic['id'],
1382                   'replyto' : -1,
1383                   'time' : to_timestamp(datetime.now(utc)),
1384                   'author' : self.author,
1385                   'body' : self.get_body_text(context.content_parts)}
[359]1386
[507]1387        ## Add message to DB and commit it.
1388        #
1389        self._add_message(api, context, message)
1390        self.db.commit()
[359]1391
[507]1392    def discussion_message_reply(self, content, subject):
[359]1393
[507]1394        ## Import modules.
1395        #
1396        from tracdiscussion.api import DiscussionApi
1397        from trac.util.datefmt import to_timestamp, utc
[359]1398
[507]1399        self.logger.debug('Replying to discussion message', self.id)
[359]1400
[507]1401        ## Get dissussion API component.
1402        #
1403        api = self.env[DiscussionApi]
1404        context = self._create_context(content, subject)
[359]1405
[507]1406        ## Get replied message.
1407        #
1408        message = api.get_message(context, self.id)
[359]1409
[507]1410        if not message:
1411            self.logger.error("ERROR: Replied message doesn't exist")
[359]1412
[507]1413        ## Prepare message.
1414        #
1415        message = {'forum' : message['forum'],
1416                   'topic' : message['topic'],
1417                   'replyto' : message['id'],
1418                   'time' : to_timestamp(datetime.now(utc)),
1419                   'author' : self.author,
1420                   'body' : self.get_body_text(context.content_parts)}
[359]1421
[507]1422        ## Add message to DB and commit it.
1423        #
1424        self._add_message(api, context, message)
1425        self.db.commit()
[359]1426
[507]1427    def _create_context(self, content, subject):
[359]1428
[507]1429        ## Import modules.
1430        #
1431        from trac.mimeview import Context
1432        from trac.web.api import Request
1433        from trac.perm import PermissionCache
[359]1434
[507]1435        ## TODO: Read server base URL from config.
1436        #  Create request object to mockup context creation.
1437        #
1438        environ = {'SERVER_PORT' : 80,
1439                   'SERVER_NAME' : 'test',
1440                   'REQUEST_METHOD' : 'POST',
1441                   'wsgi.url_scheme' : 'http',
1442                   'wsgi.input' : sys.stdin}
1443        chrome =  {'links': {},
1444                   'scripts': [],
1445                   'ctxtnav': [],
1446                   'warnings': [],
1447                   'notices': []}
[359]1448
[507]1449        if self.env.base_url_for_redirect:
1450            environ['trac.base_url'] = self.env.base_url
[359]1451
[507]1452        req = Request(environ, None)
1453        req.chrome = chrome
1454        req.tz = 'missing'
1455        req.authname = self.author
1456        req.perm = PermissionCache(self.env, self.author)
1457        req.locale = None
[359]1458
[507]1459        ## Create and return context.
1460        #
1461        context = Context.from_request(req)
1462        context.realm = 'discussion-email2trac'
1463        context.cursor = self.db.cursor()
1464        context.content = content
1465        context.subject = subject
[359]1466
[507]1467        ## Read content parts from content.
1468        #
1469        context.content_parts = self.get_message_parts(content)
1470        context.content_parts = self.unique_attachment_names(
1471          context.content_parts)
[359]1472
[507]1473        return context
[359]1474
[507]1475    def _add_topic(self, api, context, topic):
1476        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1477
[507]1478        ## Filter topic.
1479        #
1480        for discussion_filter in api.discussion_filters:
1481            accept, topic_or_error = discussion_filter.filter_topic(
1482              context, topic)
1483            if accept:
1484                topic = topic_or_error
1485            else:
1486                raise TracError(topic_or_error)
[359]1487
[507]1488        ## Add a new topic.
1489        #
1490        api.add_topic(context, topic)
[359]1491
[507]1492        ## Get inserted topic with new ID.
1493        #
1494        topic = api.get_topic_by_time(context, topic['time'])
[359]1495
[507]1496        ## Attach attachments.
1497        #
1498        self.id = topic['id']
1499        self.attach_attachments(context.content_parts, True)
[359]1500
[507]1501        ## Notify change listeners.
1502        #
1503        for listener in api.topic_change_listeners:
1504            listener.topic_created(context, topic)
[359]1505
[507]1506    def _add_message(self, api, context, message):
1507        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1508
[507]1509        ## Filter message.
1510        #
1511        for discussion_filter in api.discussion_filters:
1512            accept, message_or_error = discussion_filter.filter_message(
1513              context, message)
1514            if accept:
1515                message = message_or_error
1516            else:
1517                raise TracError(message_or_error)
[359]1518
[507]1519        ## Add message.
1520        #
1521        api.add_message(context, message)
[359]1522
[507]1523        ## Get inserted message with new ID.
1524        #
1525        message = api.get_message_by_time(context, message['time'])
[359]1526
[507]1527        ## Attach attachments.
1528        #
1529        self.id = message['topic']
1530        self.attach_attachments(context.content_parts, True)
[359]1531
[507]1532        ## Notify change listeners.
1533        #
1534        for listener in api.message_change_listeners:
1535            listener.message_created(context, message)
[359]1536
1537########## MAIN function  ######################################################
1538
[507]1539    def parse(self, fp):
1540        """
1541        """
1542        self.logger.debug('Main function parse')
1543        global m
[96]1544
[507]1545        m = email.message_from_file(fp)
1546       
1547        if not m:
1548            self.logger.debug('This is not a valid email message format')
1549            return
1550           
1551        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1552        #
1553        try:
1554            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1555        except AttributeError, detail:
1556            pass
[239]1557
[507]1558        if self.parameters.debug:     # save the entire e-mail message text
[528]1559            self.save_email_for_debug(m, self.parameters.project_name, True)
[77]1560
[507]1561        self.db = self.env.get_db_cnx()
1562        self.get_sender_info(m)
[152]1563
[507]1564        if not self.email_header_acl('white_list', self.email_addr, True):
1565            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1566            return False
[77]1567
[507]1568        if self.email_header_acl('black_list', self.email_addr, False):
1569            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1570            return False
[221]1571
[507]1572        if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1573            self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
1574            return False
[226]1575
[507]1576        ## If spam drop the message
1577        #
1578        if self.spam(m) == 'drop':
1579            return False
[194]1580
[507]1581        elif self.spam(m) == 'spam':
1582            spam_msg = True
1583        else:
1584            spam_msg = False
[204]1585
[507]1586        if not m['Subject']:
1587            subject  = 'No Subject'
1588        else:
1589            subject  = self.email_to_unicode(m['Subject'])
[304]1590
[516]1591        self.logger.info('subject: %s' %subject)
[359]1592
[507]1593        ## [hic] #1529: Re: LRZ
1594        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1595        #
1596        ticket_regex = r'''
1597            (?P<new_fields>[#][?].*)
1598            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1599            '''
1600        ## Check if  FullBlogPlugin is installed
1601        #
1602        blog_enabled = None
1603        blog_regex = ''
1604        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1605            self.logger.debug('Trac BLOG support enabled')
1606            blog_enabled = True
1607            blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1608
[77]1609
[507]1610        ## Check if DiscussionPlugin is installed
1611        #
1612        discussion_enabled = None
1613        discussion_regex = ''
1614        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1615            self.logger.debug('Trac Discussion support enabled')
1616            discussion_enabled = True
1617            discussion_regex = r'''
1618            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1619            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1620            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1621            '''
[77]1622
[359]1623
[507]1624        regex_str = ticket_regex + blog_regex + discussion_regex
1625        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
[359]1626
[507]1627        ## Find out if this is a ticket, a blog or a discussion
1628        #
1629        result =  SYSTEM_RE.search(subject)
[390]1630
[507]1631        if result:
1632            ## update ticket + fields
1633            #
1634            if result.group('reply'):
1635                self.system = 'ticket'
[260]1636
[507]1637                ## Skip the last ':' character
1638                #
1639                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1640                    self.new_ticket(m, subject, spam_msg)
[390]1641
[507]1642            ## New ticket + fields
1643            #
1644            elif result.group('new_fields'):
1645                self.system = 'ticket'
1646                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
[262]1647
[507]1648            if blog_enabled:
1649                if result.group('blog'):
1650                    self.system = 'blog'
1651                    self.blog(result.group('blog_id'))
[359]1652
[507]1653            if discussion_enabled:
1654                ## New topic.
1655                #
1656                if result.group('forum'):
1657                    self.system = 'discussion'
1658                    self.id = int(result.group('forum_id'))
1659                    self.discussion_topic(m, subject[result.end('forum'):])
[359]1660
[507]1661                ## Reply to topic.
1662                #
1663                elif result.group('topic'):
1664                    self.system = 'discussion'
1665                    self.id = int(result.group('topic_id'))
1666                    self.discussion_topic_reply(m, subject[result.end('topic'):])
[359]1667
[507]1668                ## Reply to topic message.
1669                #
1670                elif result.group('message'):
1671                    self.system = 'discussion'
1672                    self.id = int(result.group('message_id'))
1673                    self.discussion_message_reply(m, subject[result.end('message'):])
[359]1674
[507]1675        else:
1676            self.system = 'ticket'
1677            (matched_id, subject) = self.ticket_update_by_subject(subject)
1678            if matched_id:
1679                if not self.ticket_update(m, matched_id, spam_msg):
1680                    self.new_ticket(m, subject, spam_msg)
1681            else:
1682                ## No update by subject, so just create a new ticket
1683                #
1684                self.new_ticket(m, subject, spam_msg)
[353]1685
[356]1686
[343]1687########## BODY TEXT functions  ###########################################################
1688
[507]1689    def strip_signature(self, text):
1690        """
1691        Strip signature from message, inspired by Mailman software
1692        """
1693        self.logger.debug('function strip_signature')
[462]1694
[507]1695        body = []
1696        for line in text.splitlines():
1697            if line == '-- ':
1698                break
1699            body.append(line)
[136]1700
[507]1701        return ('\n'.join(body))
[136]1702
[507]1703    def reflow(self, text, delsp = 0):
1704        """
1705        Reflow the message based on the format="flowed" specification (RFC 3676)
1706        """
1707        flowedlines = []
1708        quotelevel = 0
1709        prevflowed = 0
[231]1710
[507]1711        for line in text.splitlines():
1712            from re import match
1713           
1714            ## Figure out the quote level and the content of the current line
1715            #
1716            m = match('(>*)( ?)(.*)', line)
1717            linequotelevel = len(m.group(1))
1718            line = m.group(3)
[231]1719
[507]1720            ## Determine whether this line is flowed
1721            #
1722            if line and line != '-- ' and line[-1] == ' ':
1723                flowed = 1
1724            else:
1725                flowed = 0
[231]1726
[507]1727            if flowed and delsp and line and line[-1] == ' ':
1728                line = line[:-1]
[231]1729
[507]1730            ## If the previous line is flowed, append this line to it
1731            #
1732            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1733                flowedlines[-1] += line
[477]1734
[507]1735            ## Otherwise, start a new line
1736            #
1737            else:
1738                flowedlines.append('>' * linequotelevel + line)
[231]1739
[507]1740            prevflowed = flowed
1741           
[231]1742
[507]1743        return '\n'.join(flowedlines)
[231]1744
[507]1745    def strip_quotes(self, text):
1746        """
1747        Strip quotes from message by Nicolas Mendoza
1748        """
1749        self.logger.debug('function strip_quotes')
[462]1750
[507]1751        body = []
1752        for line in text.splitlines():
1753            try:
[462]1754
[507]1755                if line.startswith(self.parameters.email_quote):
1756                    continue
[462]1757
[507]1758            except UnicodeDecodeError:
[462]1759
[507]1760                tmp_line = self.email_to_unicode(line)
1761                if tmp_line.startswith(self.parameters.email_quote):
1762                    continue
1763               
1764            body.append(line)
[151]1765
[507]1766        return ('\n'.join(body))
[191]1767
[507]1768    def inline_properties(self, text):
1769        """
1770        Parse text if we use inline keywords to set ticket fields
1771        """
1772        self.logger.debug('function inline_properties')
[309]1773
[507]1774        properties = dict()
1775        body = list()
[309]1776
[507]1777        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
[309]1778
[507]1779        for line in text.splitlines():
1780            match = INLINE_EXP.match(line)
1781            if match:
1782                keyword, value = match.groups()
[509]1783
1784                if self.parameters.inline_properties_first_wins:
1785                    if keyword in self.properties.keys():
1786                        continue
1787
[507]1788                self.properties[keyword] = value.strip()
[509]1789                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
[408]1790
[507]1791            else:
1792                body.append(line)
1793               
1794        return '\n'.join(body)
[309]1795
1796
[507]1797    def wrap_text(self, text, replace_whitespace = False):
1798        """
1799        Will break a lines longer then given length into several small
1800        lines of size given length
1801        """
1802        import textwrap
[154]1803
[507]1804        LINESEPARATOR = '\n'
1805        reformat = ''
[151]1806
[507]1807        for s in text.split(LINESEPARATOR):
1808            tmp = textwrap.fill(s, self.parameters.use_textwrap)
1809            if tmp:
1810                reformat = '%s\n%s' %(reformat,tmp)
1811            else:
1812                reformat = '%s\n' %reformat
[153]1813
[507]1814        return reformat
[153]1815
[507]1816        # Python2.4 and higher
1817        #
1818        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1819        #
[154]1820
[343]1821########## EMAIL attachements functions ###########################################################
1822
[507]1823    def inline_part(self, part):
1824        """
1825        """
1826        self.logger.debug('function inline_part()')
[154]1827
[507]1828        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
[340]1829
[507]1830    def get_message_parts(self, msg):
1831        """
1832        parses the email message and returns a list of body parts and attachments
1833        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1834        """
1835        self.logger.debug('function get_message_parts()')
[317]1836
[507]1837        message_parts = list()
1838   
1839        ALTERNATIVE_MULTIPART = False
[278]1840
[507]1841        for part in msg.walk():
1842            content_maintype = part.get_content_maintype()
1843            content_type =  part.get_content_type()
[278]1844
[507]1845            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
1846            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
[498]1847
[507]1848            ## Check content type
1849            #
1850            if content_type in self.STRIP_CONTENT_TYPES:
1851                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
1852                continue
[238]1853
[507]1854            ## Catch some mulitpart execptions
1855            #
1856            if content_type == 'multipart/alternative':
1857                ALTERNATIVE_MULTIPART = True
1858                continue
[278]1859
[507]1860            ## Skip multipart containers
1861            #
1862            if content_maintype == 'multipart':
1863                self.logger.debug("\t Skipping multipart container")
1864                continue
1865           
1866            ## 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"
1867            #
1868            inline = self.inline_part(part)
[236]1869
[507]1870            ## Drop HTML message
1871            #
1872            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
1873                if content_type == 'text/html':
1874                    self.logger.debug('\t Skipping alternative HTML message')
1875                    ALTERNATIVE_MULTIPART = False
1876                    continue
[278]1877
[498]1878
[507]1879            ## Save all non plain text message as attachment
1880            #
1881            if not content_type in ['text/plain']:
[498]1882
[507]1883                if self.parameters.debug:
1884                    s = '\t Filename: %s' % part.get_filename()
1885                    self.print_unicode(s)
[498]1886
[507]1887                ## First try to use email header function to convert filename.
1888                #  If this fails the use the plain filename
1889                #
1890                try:
1891                    filename = self.email_to_unicode(part.get_filename())
1892                except UnicodeEncodeError, detail:
1893                    filename = part.get_filename()
[498]1894
[507]1895                message_parts.append((filename, part))
[498]1896
[507]1897                ## We van only convert html messages
1898                #
1899                if not content_type == 'text/html':
1900                    self.logger.debug('\t Appending content_type = %s' %(content_type))
1901                    continue
[500]1902
[507]1903            if not inline:
1904                    self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type))
1905                    continue
1906               
1907            ## Try to decode message part. We have a html or plain text messafe
1908            #
1909            body_text = part.get_payload(decode=1)
1910            if not body_text:           
1911                body_text = part.get_payload(decode=0)
[236]1912
[507]1913            ## Try to convert html message
1914            #
1915            if content_type == 'text/html':
[502]1916
[507]1917                body_text = self.html_2_txt(body_text)
1918                if not body_text:
1919                    continue
[498]1920
[507]1921            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1922            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1923
[507]1924            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
1925                body_text = self.reflow(body_text, delsp == 'yes')
1926   
1927            if self.parameters.strip_signature:
1928                body_text = self.strip_signature(body_text)
[22]1929
[507]1930            if self.parameters.strip_quotes:
1931                body_text = self.strip_quotes(body_text)
[191]1932
[507]1933            if self.parameters.inline_properties:
1934                body_text = self.inline_properties(body_text)
[309]1935
[507]1936            if self.parameters.use_textwrap:
1937                body_text = self.wrap_text(body_text)
[148]1938
[507]1939            ## Get contents charset (iso-8859-15 if not defined in mail headers)
1940            #
1941            charset = part.get_content_charset()
1942            if not charset:
1943                charset = 'iso-8859-15'
[102]1944
[507]1945            try:
1946                ubody_text = unicode(body_text, charset)
[100]1947
[507]1948            except UnicodeError, detail:
1949                ubody_text = unicode(body_text, 'iso-8859-15')
[89]1950
[507]1951            except LookupError, detail:
1952                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1953
[507]1954            if self.parameters.verbatim_format:
1955                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1956            else:
1957                message_parts.append('%s' %ubody_text)
[22]1958
[507]1959        return message_parts
1960       
1961    def unique_attachment_names(self, message_parts):
1962        """
1963        Make sure we have unique names attachments:
1964          - check if it contains illegal characters
1965          - Rename "None" filenames to "untitled-part"
1966        """
1967        self.logger.debug('function unique_attachment_names()')
1968        renamed_parts = []
1969        attachment_names = set()
[296]1970
[507]1971        for item in message_parts:
1972           
1973            ## If not an attachment, leave it alone
1974            #
1975            if not isinstance(item, tuple):
1976                renamed_parts.append(item)
1977                continue
1978               
1979            (filename, part) = item
[295]1980
[507]1981            ## If filename = None, use a default one
1982            #
1983            if filename in [ 'None']:
1984                filename = 'untitled-part'
1985                self.logger.info('Rename filename "None" to: %s' %filename)
[22]1986
[507]1987                ## Guess the extension from the content type, use non strict mode
1988                #  some additional non-standard but commonly used MIME types
1989                #  are also recognized
1990                #
1991                ext = mimetypes.guess_extension(part.get_content_type(), False)
1992                if not ext:
1993                    ext = '.bin'
[22]1994
[507]1995                filename = '%s%s' % (filename, ext)
[22]1996
[507]1997            ## Discard relative paths for windows/unix in attachment names
1998            #
1999            #filename = filename.replace('\\', '/').replace(':', '/')
2000            filename = filename.replace('\\', '_')
2001            filename = filename.replace('/', '_')
[347]2002
[507]2003            ## remove linefeed char
2004            #
2005            for forbidden_char in ['\r', '\n']:
2006                filename = filename.replace(forbidden_char,'')
[465]2007
[507]2008            ## We try to normalize the filename to utf-8 NFC if we can.
2009            #  Files uploaded from OS X might be in NFD.
2010            #  Check python version and then try it
2011            #
2012            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2013            #   try:
2014            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2015            #   except TypeError:
2016            #       pass
[100]2017
[507]2018            ## Make the filename unique for this ticket
2019            #
2020            num = 0
2021            unique_filename = filename
2022            dummy_filename, ext = os.path.splitext(filename)
[134]2023
[507]2024            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2025                num += 1
2026                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2027               
2028            if self.parameters.debug:
2029                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2030                self.print_unicode(s)
[100]2031
[507]2032            attachment_names.add(unique_filename)
[236]2033
[507]2034            renamed_parts.append((filename, unique_filename, part))
2035   
2036        return renamed_parts
2037           
2038           
2039    def attachment_exists(self, filename):
[250]2040
[507]2041        if self.parameters.debug:
2042            s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2043            self.print_unicode(s)
[250]2044
[507]2045        ## We have no valid ticket id
2046        #
2047        if not self.id:
2048            return False
[250]2049
[507]2050        try:
2051            if self.system == 'discussion':
2052                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
2053                  % (self.id,), filename)
2054            else:
2055                att = attachment.Attachment(self.env, 'ticket', self.id,
2056                  filename)
2057            return True
2058        except attachment.ResourceNotFound:
2059            return False
[343]2060
2061########## TRAC Ticket Text ###########################################################
[507]2062           
2063    def get_body_text(self, message_parts):
2064        """
2065        """
2066        self.logger.debug('function get_body_text()')
[498]2067
[507]2068        body_text = []
2069       
2070        for part in message_parts:
2071       
2072            ## Plain text part, append it
2073            #
2074            if not isinstance(part, tuple):
2075                body_text.extend(part.strip().splitlines())
2076                body_text.append("")
2077                continue
[490]2078
[507]2079            (original, filename, part) = part
2080            inline = self.inline_part(part)
[491]2081
[507]2082            ## Skip generation of attachment link if html is converted to text
2083            #
2084            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2085                s = 'Skipping attachment link for html part: %s' %(filename)
2086                self.print_unicode(s)
2087                continue
2088           
2089            if part.get_content_maintype() == 'image' and inline:
2090                if self.system != 'discussion':
2091                    s = 'wiki image link for: %s' %(filename)
2092                    self.print_unicode(s)
2093                    body_text.append('[[Image(%s)]]' % filename)
2094                body_text.append("")
2095            else:
2096                if self.system != 'discussion':
2097                    s = 'wiki attachment link for: %s' %(filename)
2098                    self.print_unicode(s)
2099                    body_text.append('[attachment:"%s"]' % filename)
2100                body_text.append("")
[490]2101
[507]2102        ## Convert list body_texts to string
2103        #
2104        body_text = '\r\n'.join(body_text)
2105        return body_text
[236]2106
[507]2107    def html_mailto_link(self, subject):
2108        """
2109        This function returns a HTML mailto tag with the ticket id and author email address
2110        """
2111        self.logger.debug("function html_mailto_link")
2112        if not self.author:
2113            author = self.email_addr
2114        else:   
2115            author = self.author
[343]2116
[507]2117        if not self.parameters.mailto_cc:
2118            self.parameters.mailto_cc = ''
[434]2119
[507]2120        ## use urllib to escape the chars
2121        #
[517]2122        s = '%s?Subject=%s&cc=%s' %(
[507]2123               urllib.quote(self.email_addr),
2124               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2125               urllib.quote(self.parameters.mailto_cc)
2126               )
[343]2127
[507]2128        if self.VERSION in [ 0.10 ]:
2129            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)
2130        else:
2131            s = '[mailto:"%s" Reply to: %s]' %(s, author)
[506]2132
[507]2133        self.logger.debug("\tmailto link %s" %s)
2134        return s
[343]2135
2136########## TRAC notify section ###########################################################
2137
[507]2138    def notify(self, tkt, new=True, modtime=0):
2139        """
2140        A wrapper for the TRAC notify function. So we can use templates
2141        """
2142        self.logger.debug('function notify()')
[392]2143
[509]2144        if self.parameters.notify_reporter:
2145            self.logger.debug('\t Notify reporter set')
[508]2146            global sender_email
2147            sender_email = self.email_addr
2148 
2149            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2150            import trac.notification as Email2TracNotification
2151            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2152
2153        if self.parameters.dry_run  :
[507]2154                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
2155                return
2156        try:
[392]2157
[507]2158            #from trac.ticket.web_ui import TicketModule
2159            #from trac.ticket.notification import TicketNotificationSystem
2160            #ticket_sys = TicketNotificationSystem(self.env)
2161            #a = TicketModule(self.env)
2162            #print a.__dict__
2163            #tn_sys = TicketNotificationSystem(self.env)
2164            #print tn_sys
2165            #print tn_sys.__dict__
2166            #sys.exit(0)
[392]2167
[507]2168            ## create false {abs_}href properties, to trick Notify()
2169            #
2170            if not (self.VERSION in [0.11, 0.12]):
2171                self.env.abs_href = Href(self.get_config('project', 'url'))
2172                self.env.href = Href(self.get_config('project', 'url'))
[22]2173
[507]2174            tn = TicketNotifyEmail(self.env)
[213]2175
[507]2176            if self.parameters.alternate_notify_template:
[222]2177
[507]2178                if self.VERSION >= 0.11:
[222]2179
[507]2180                    from trac.web.chrome import Chrome
[222]2181
[507]2182                    if  self.parameters.alternate_notify_template_update and not new:
2183                        tn.template_name = self.parameters.alternate_notify_template_update
2184                    else:
2185                        tn.template_name = self.parameters.alternate_notify_template
[222]2186
[507]2187                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2188                       
2189                else:
[222]2190
[507]2191                    tn.template_name = self.parameters.alternate_notify_template
[42]2192
[507]2193            tn.notify(tkt, new, modtime)
[41]2194
[507]2195        except Exception, e:
2196            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2197
[508]2198########## END Class Definition  ########################################################
[22]2199
[508]2200########## Global Notificaition Function ################################################
2201def AlwaysNotifyReporter(self, resid):
2202    """
2203    Copy of def notify() to manipulate recipents to always include reporter for the
2204    notification.
2205    """
2206    (torcpts, ccrcpts) = self.get_recipients(resid)
[74]2207
[508]2208    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2209        ## additionally append sender (regardeless of settings in trac.ini)
2210        #
2211        if not sender_email in torcpts:
2212            torcpts.append(sender_email)
2213
2214    self.begin_send()
2215    self.send(torcpts, ccrcpts)
2216    self.finish_send()
2217
[343]2218########## Parse Config File  ###########################################################
[22]2219
2220def ReadConfig(file, name):
[507]2221    """
2222    Parse the config file
2223    """
2224    if not os.path.isfile(file):
2225        print 'File %s does not exist' %file
2226        sys.exit(1)
[22]2227
[507]2228    config = trac_config.Configuration(file)
2229   
2230    parentdir = config.get('DEFAULT', 'parentdir')
2231    sections = config.sections()
[22]2232
[507]2233    ## use some trac internals to get the defaults
2234    #
2235    tmp = config.parser.defaults()
2236    project =  SaraDict()
[485]2237
[507]2238    for option, value in tmp.items():
2239        try:
2240            project[option] = int(value)
2241        except ValueError:
2242            project[option] = value
[485]2243
[507]2244    if name:
2245        if name in sections:
2246            project =  SaraDict()
2247            for option, value in  config.options(name):
2248                try:
2249                    project[option] = int(value)
2250                except ValueError:
2251                    project[option] = value
[485]2252
[507]2253        elif not parentdir:
2254            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2255            print "or set parentdir in the [DEFAULT] section"
2256            sys.exit(1)
[22]2257
[507]2258    ## If parentdir then set project dir to parentdir + name
2259    #
2260    if not project.has_key('project'):
2261        if not parentdir:
2262            print "You must set project or parentdir in your configuration file"
2263            sys.exit(1)
2264        elif not name:
2265            print "You must configure a  project section in your configuration file"
2266        else:
2267            project['project'] = os.path.join(parentdir, name)
[22]2268
[528]2269    ##
2270    # Save the project name
2271    #
2272    project['project_name'] = os.path.basename(project['project'])
2273
[507]2274    return project
[22]2275
[410]2276########## Setup Logging ###############################################################
[87]2277
[419]2278def setup_log(parameters, project_name, interactive=None):
[507]2279    """
[516]2280    Setup logging
[410]2281
[507]2282    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
[420]2283    would be interpreted by the ConfigParser itself.
[507]2284    """
2285    logger = logging.getLogger('email2trac %s' %project_name)
[410]2286
[507]2287    if interactive:
2288        parameters.log_type = 'stderr'
[414]2289
[507]2290    if not parameters.log_type:
2291        if sys.platform in ['win32', 'cygwin']:
[521]2292            parameters.log_type = 'eventlog'
[507]2293        else:
2294            parameters.log_type = 'syslog'
[410]2295
[507]2296    if parameters.log_type == 'file':
[415]2297
[507]2298        if not parameters.log_file:
2299            parameters.log_file = 'email2trac.log'
[415]2300
[507]2301        if not os.path.isabs(parameters.log_file):
2302            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
[410]2303
[507]2304        log_handler = logging.FileHandler(parameters.log_file)
[415]2305
[507]2306    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2307        ## Requires win32 extensions
2308        #
2309        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
[410]2310
[507]2311    elif parameters.log_type in ('syslog', 'unix'):
2312        log_handler = logging.handlers.SysLogHandler('/dev/log')
[410]2313
[507]2314    elif parameters.log_type in ('stderr'):
2315        log_handler = logging.StreamHandler(sys.stderr)
[410]2316
[507]2317    else:
2318        log_handler = logging.handlers.BufferingHandler(0)
[410]2319
[507]2320    if parameters.log_format:
2321        parameters.log_format = parameters.log_format.replace('$(', '%(')
2322    else:
2323        parameters.log_format = '%(name)s: %(message)s'
[516]2324        if parameters.log_type in ('file', 'stderr'):
2325            parameters.log_format = '%(asctime)s ' + parameters.log_format
[410]2326
[507]2327    log_formatter = logging.Formatter(parameters.log_format)
2328    log_handler.setFormatter(log_formatter)
2329    logger.addHandler(log_handler)
[410]2330
[507]2331    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2332        logger.setLevel(logging.DEBUG)
2333        parameters.debug = 1
[410]2334
[507]2335    elif parameters.log_level in ['INFO'] or parameters.verbose:
2336        logger.setLevel(logging.INFO)
[410]2337
[507]2338    elif parameters.log_level in ['WARNING']:
2339        logger.setLevel(logging.WARNING)
[410]2340
[507]2341    elif parameters.log_level in ['ERROR']:
2342        logger.setLevel(logging.ERROR)
[410]2343
[507]2344    elif parameters.log_level in ['CRITICAL']:
2345        logger.setLevel(logging.CRITICAL)
[410]2346
[507]2347    else:
2348        logger.setLevel(logging.INFO)
[410]2349
[507]2350    return logger
[410]2351
2352
[22]2353if __name__ == '__main__':
[507]2354    ## Default config file
2355    #
2356    configfile = '@email2trac_conf@'
2357    project = ''
2358    component = ''
2359    ticket_prefix = 'default'
2360    dry_run = None
2361    verbose = None
2362    debug_interactive = None
[202]2363
[507]2364    SHORT_OPT = 'cdhf:np:t:v'
2365    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2366
[507]2367    try:
2368        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2369    except getopt.error,detail:
2370        print __doc__
2371        print detail
2372        sys.exit(1)
2373   
2374    project_name = None
2375    for opt,value in opts:
2376        if opt in [ '-h', '--help']:
2377            print __doc__
2378            sys.exit(0)
2379        elif opt in ['-c', '--component']:
2380            component = value
2381        elif opt in ['-d', '--debug']:
2382            debug_interactive = 1
2383        elif opt in ['-f', '--file']:
2384            configfile = value
2385        elif opt in ['-n', '--dry-run']:
2386            dry_run = True
2387        elif opt in ['-p', '--project']:
2388            project_name = value
2389        elif opt in ['-t', '--ticket_prefix']:
2390            ticket_prefix = value
2391        elif opt in ['-v', '--verbose']:
2392            verbose = True
2393   
2394    settings = ReadConfig(configfile, project_name)
[410]2395
[507]2396    ## The default prefix for ticket values in email2trac.conf
2397    #
2398    settings.ticket_prefix = ticket_prefix
2399    settings.dry_run = dry_run
2400    settings.verbose = verbose
[410]2401
[507]2402    if not settings.debug and debug_interactive:
2403        settings.debug = debug_interactive
[412]2404
[507]2405    if not settings.project:
2406        print __doc__
2407        print 'No Trac project is defined in the email2trac config file.'
2408        sys.exit(1)
[419]2409
[507]2410    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2411   
2412    if component:
2413        settings['component'] = component
[202]2414
[507]2415    ## Determine major trac version used to be in email2trac.conf
2416    # Quick hack for 0.12
2417    #
2418    version = '0.%s' %(trac_version.split('.')[1])
2419    if version.startswith('0.12'):
2420        version = '0.12'
[373]2421
[507]2422    logger.debug("Found trac version: %s" %(version))
2423   
2424    try:
2425        if version == '0.10':
2426            from trac import attachment
2427            from trac.env import Environment
2428            from trac.ticket import Ticket
2429            from trac.web.href import Href
2430            from trac import util
2431            from trac.ticket.web_ui import TicketModule
[486]2432
[507]2433            #
2434            # return  util.text.to_unicode(str)
2435            #
2436            # see http://projects.edgewall.com/trac/changeset/2799
2437            from trac.ticket.notification import TicketNotifyEmail
2438            from trac import config as trac_config
2439            from trac.core import TracError
[359]2440
[507]2441        elif version == '0.11':
2442            from trac import attachment
2443            from trac.env import Environment
2444            from trac.ticket import Ticket
2445            from trac.web.href import Href
2446            from trac import config as trac_config
2447            from trac import util
2448            from trac.core import TracError
2449            from trac.perm import PermissionSystem
2450            from trac.ticket.web_ui import TicketModule
[260]2451
[507]2452            #
2453            # return  util.text.to_unicode(str)
2454            #
2455            # see http://projects.edgewall.com/trac/changeset/2799
2456            from trac.ticket.notification import TicketNotifyEmail
[260]2457
[507]2458        elif version == '0.12':
2459            from trac import attachment
2460            from trac.env import Environment
2461            from trac.ticket import Ticket
2462            from trac.web.href import Href
2463            from trac import config as trac_config
2464            from trac import util
2465            from trac.core import TracError
2466            from trac.perm import PermissionSystem
2467            from trac.ticket.web_ui import TicketModule
[368]2468
[507]2469            #
2470            # return  util.text.to_unicode(str)
2471            #
2472            # see http://projects.edgewall.com/trac/changeset/2799
2473            from trac.ticket.notification import TicketNotifyEmail
[368]2474
2475
[507]2476        else:
2477            logger.error('TRAC version %s is not supported' %version)
[519]2478            sys.exit(0)
[182]2479
[507]2480        ## Must be set before environment is created
2481        #
2482        if settings.has_key('python_egg_cache'):
2483            python_egg_cache = str(settings['python_egg_cache'])
2484            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
[291]2485
[507]2486        if settings.debug > 0:
2487            logger.debug('Loading environment %s', settings.project)
[359]2488
[507]2489        try:
2490            env = Environment(settings['project'], create=0)
2491        except IOError, detail:
[519]2492            logger.error("Trac project does not exists: %s" %settings['project'])
2493            sys.exit(0)
[333]2494
[507]2495        tktparser = TicketEmailParser(env, settings, logger, float(version))
2496        tktparser.parse(sys.stdin)
[22]2497
[507]2498    ## Catch all errors and use the logging module
2499    #
2500    except Exception, error:
[187]2501
[507]2502        etype, evalue, etb = sys.exc_info()
2503        for e in traceback.format_exception(etype, evalue, etb):
2504            logger.critical(e)
[187]2505
[507]2506        if m:
[528]2507            tktparser.save_email_for_debug(m, settings.project_name, True)
[97]2508
[507]2509        sys.exit(1)
[22]2510# EOB
Note: See TracBrowser for help on using the repository browser.