source: trunk/email2trac.py.in @ 563

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

fixed unicode error when printing subject line. Python logging module problem, closes #267

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