source: trunk/email2trac.py.in @ 540

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

preparing email2trac for trac 0.13 release, see #250

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