source: trunk/email2trac.py.in @ 557

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

delete obsolete workflow code, see #226

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