source: trunk/email2trac.py.in @ 556

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

chamged workflow to TicketModule? implementation as suggested by: Holger Jürgs, see #226

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