source: trunk/email2trac.py.in @ 572

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

adjusted email_header_acl we now loop over all to address to allow regexs like (*.sara.nl or basv@…), see #272

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