source: trunk/email2trac.py.in @ 543

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

comment permission check for workflow. Is already done and some minor reformating of code

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