source: trunk/email2trac.py.in @ 309

Last change on this file since 309 was 309, checked in by bas, 14 years ago

email2trac.py.in:

  • added inline properties patch. To set ticket fields within an email, eg:

@owner: bas

Will set the owner of the ticket to bas, closes #171

  • Display a warning if it is a blog message and the plugin is not installed, see #175


  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 42.0 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[297]44    trac_version : 0.10              # OPTIONAL, default is 0.11
[22]45
[282]46    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
47    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]48       
49 * default config file is : /etc/email2trac.conf
50
51 * Commandline opions:
[205]52                -h,--help
53                -f,--file  <configuration file>
54                -n,--dry-run
55                -p, --project <project name>
56                -t, --ticket_prefix <name>
[22]57
58SVN Info:
59        $Id: email2trac.py.in 309 2010-02-02 15:39:19Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import stat
66import time
67import email
[136]68import email.Iterators
69import email.Header
[22]70import re
71import urllib
72import unicodedata
73from stat import *
74import mimetypes
[96]75import traceback
[22]76
[190]77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
[182]86from datetime import tzinfo, timedelta, datetime
[199]87from trac import config as trac_config
[91]88
[96]89# Some global variables
90#
[276]91trac_default_version = '0.11'
[96]92m = None
[22]93
[182]94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
[22]112class TicketEmailParser(object):
113        env = None
114        comment = '> '
115   
[206]116        def __init__(self, env, parameters, version):
[22]117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
[206]123                # Save parameters
124                #
125                self.parameters = parameters
126
[72]127                # Some useful mail constants
128                #
[287]129                self.email_name = None
[72]130                self.email_addr = None
[183]131                self.email_from = None
[288]132                self.author     = None
[287]133                self.id         = None
[294]134               
135                self.STRIP_CONTENT_TYPES = list()
[72]136
[22]137                self.VERSION = version
[206]138                self.DRY_RUN = parameters['dry_run']
[204]139
[172]140                self.get_config = self.env.config.get
[22]141
142                if parameters.has_key('umask'):
143                        os.umask(int(parameters['umask'], 8))
144
145                if parameters.has_key('debug'):
146                        self.DEBUG = int(parameters['debug'])
147                else:
148                        self.DEBUG = 0
149
150                if parameters.has_key('mailto_link'):
151                        self.MAILTO = int(parameters['mailto_link'])
[74]152                        if parameters.has_key('mailto_cc'):
153                                self.MAILTO_CC = parameters['mailto_cc']
154                        else:
155                                self.MAILTO_CC = ''
[22]156                else:
157                        self.MAILTO = 0
158
159                if parameters.has_key('spam_level'):
160                        self.SPAM_LEVEL = int(parameters['spam_level'])
161                else:
162                        self.SPAM_LEVEL = 0
163
[207]164                if parameters.has_key('spam_header'):
165                        self.SPAM_HEADER = parameters['spam_header']
166                else:
167                        self.SPAM_HEADER = 'X-Spam-Score'
168
[191]169                if parameters.has_key('email_quote'):
170                        self.EMAIL_QUOTE = str(parameters['email_quote'])
171                else:   
172                        self.EMAIL_QUOTE = '> '
[22]173
174                if parameters.has_key('email_header'):
175                        self.EMAIL_HEADER = int(parameters['email_header'])
176                else:
177                        self.EMAIL_HEADER = 0
178
[42]179                if parameters.has_key('alternate_notify_template'):
180                        self.notify_template = str(parameters['alternate_notify_template'])
181                else:
182                        self.notify_template = None
[22]183
[222]184                if parameters.has_key('alternate_notify_template_update'):
185                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
186                else:
187                        self.notify_template_update = None
188
[43]189                if parameters.has_key('reply_all'):
190                        self.REPLY_ALL = int(parameters['reply_all'])
191                else:
192                        self.REPLY_ALL = 0
[42]193
[74]194                if parameters.has_key('ticket_update'):
195                        self.TICKET_UPDATE = int(parameters['ticket_update'])
196                else:
197                        self.TICKET_UPDATE = 0
[43]198
[118]199                if parameters.has_key('drop_spam'):
200                        self.DROP_SPAM = int(parameters['drop_spam'])
201                else:
202                        self.DROP_SPAM = 0
[74]203
[134]204                if parameters.has_key('verbatim_format'):
205                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
206                else:
207                        self.VERBATIM_FORMAT = 1
[118]208
[231]209                if parameters.has_key('reflow'):
[256]210                        self.REFLOW = int(parameters['reflow'])
[231]211                else:
212                        self.REFLOW = 1
213
[278]214                if parameters.has_key('drop_alternative_html_version'):
215                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
216                else:
217                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
218
[136]219                if parameters.has_key('strip_signature'):
220                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
221                else:
222                        self.STRIP_SIGNATURE = 0
[134]223
[191]224                if parameters.has_key('strip_quotes'):
225                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
226                else:
227                        self.STRIP_QUOTES = 0
228
[309]229                self.properties = dict()
230                if parameters.has_key('inline_properties'):
231                        self.INLINE_PROPERTIES = int(parameters['inline_properties'])
232                else:
233                        self.INLINE_PROPERTIES = 0
234
[148]235                if parameters.has_key('use_textwrap'):
236                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
237                else:
238                        self.USE_TEXTWRAP = 0
239
[238]240                if parameters.has_key('binhex'):
[294]241                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]242
243                if parameters.has_key('applesingle'):
[294]244                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]245
246                if parameters.has_key('appledouble'):
[294]247                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]248
[294]249                if parameters.has_key('strip_content_types'):
250                        items = parameters['strip_content_types'].split(',')
251                        for item in items:
252                                self.STRIP_CONTENT_TYPES.append(item.strip())
253
[257]254                self.WORKFLOW = None
255                if parameters.has_key('workflow'):
256                        self.WORKFLOW = parameters['workflow']
257
[173]258                # Use OS independend functions
259                #
260                self.TMPDIR = os.path.normcase('/tmp')
261                if parameters.has_key('tmpdir'):
262                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
263
[194]264                if parameters.has_key('ignore_trac_user_settings'):
265                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
266                else:
267                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]268
[297]269                if parameters.has_key('subject_field_separator'):
270                        self.SUBJECT_FIELD_SEPARATOR = parameters['subject_field_separator'].strip()
271                else:
272                        self.SUBJECT_FIELD_SEPARATOR = '&'
273
[305]274                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
275
[22]276        def spam(self, message):
[191]277                """
278                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
279                # Note if Spam_level then '*' are included
280                """
[194]281                spam = False
[207]282                if message.has_key(self.SPAM_HEADER):
283                        spam_l = string.split(message[self.SPAM_HEADER])
[22]284
[207]285                        try:
286                                number = spam_l[0].count('*')
287                        except IndexError, detail:
288                                number = 0
289                               
[22]290                        if number >= self.SPAM_LEVEL:
[194]291                                spam = True
292                               
[191]293                # treat virus mails as spam
294                #
295                elif message.has_key('X-Virus-found'):                 
[194]296                        spam = True
297
298                # How to handle SPAM messages
299                #
300                if self.DROP_SPAM and spam:
301                        if self.DEBUG > 2 :
302                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
303
[204]304                        return 'drop'   
[194]305
306                elif spam:
307
[204]308                        return 'Spam'   
[67]309
[194]310                else:
[22]311
[204]312                        return False
[191]313
[221]314        def email_header_acl(self, keyword, header_field, default):
[206]315                """
[221]316                This function wil check if the email address is allowed or denied
317                to send mail to the ticket list
318            """
[206]319                try:
[221]320                        mail_addresses = self.parameters[keyword]
321
322                        # Check if we have an empty string
323                        #
324                        if not mail_addresses:
325                                return default
326
[206]327                except KeyError, detail:
[221]328                        if self.DEBUG > 2 :
[250]329                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
[206]330
[221]331                        return default
[206]332
[221]333                mail_addresses = string.split(mail_addresses, ',')
334
335                for entry in mail_addresses:
[209]336                        entry = entry.strip()
[221]337                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
338                        result =  TO_RE.search(header_field)
[208]339                        if result:
340                                return True
[149]341
[208]342                return False
343
[139]344        def email_to_unicode(self, message_str):
[22]345                """
346                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]347        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]348                understands it.
349                """
[139]350                results =  email.Header.decode_header(message_str)
[288]351                s = None
[22]352                for text,format in results:
353                        if format:
354                                try:
355                                        temp = unicode(text, format)
[139]356                                except UnicodeError, detail:
[22]357                                        # This always works
358                                        #
359                                        temp = unicode(text, 'iso-8859-15')
[139]360                                except LookupError, detail:
361                                        #text = 'ERROR: Could not find charset: %s, please install' %format
362                                        #temp = unicode(text, 'iso-8859-15')
363                                        temp = message_str
364                                       
[22]365                        else:
366                                temp = string.strip(text)
[92]367                                temp = unicode(text, 'iso-8859-15')
[22]368
[288]369                        if s:
370                                s = '%s %s' %(s, temp)
[22]371                        else:
[288]372                                s = '%s' %temp
[22]373
[288]374                #s = s.encode('utf-8')
375                return s
[22]376
[236]377        def debug_body(self, message_body, tempfile=False):
378                if tempfile:
379                        import tempfile
380                        body_file = tempfile.mktemp('.email2trac')
381                else:
382                        body_file = os.path.join(self.TMPDIR, 'body.txt')
383
384                print 'TD: writing body (%s)' % body_file
385                fx = open(body_file, 'wb')
386                if not message_body:
387                        message_body = '(None)'
[274]388
389                message_body = message_body.encode('utf-8')
390                #message_body = unicode(message_body, 'iso-8859-15')
391
[236]392                fx.write(message_body)
393                fx.close()
394                try:
395                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
396                except OSError:
397                        pass
398
399        def debug_attachments(self, message_parts):
[22]400                n = 0
[236]401                for part in message_parts:
402                        # Skip inline text parts
403                        if not isinstance(part, tuple):
[22]404                                continue
[236]405                               
[237]406                        (original, filename, part) = part
[22]407
408                        n = n + 1
409                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
410                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
411
[236]412                        part_file = os.path.join(self.TMPDIR, filename)
[173]413                        #part_file = '/var/tmp/part%d' % n
[22]414                        print 'TD: writing part%d (%s)' % (n,part_file)
415                        fx = open(part_file, 'wb')
416                        text = part.get_payload(decode=1)
417                        if not text:
418                                text = '(None)'
419                        fx.write(text)
420                        fx.close()
421                        try:
422                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
423                        except OSError:
424                                pass
425
426        def email_header_txt(self, m):
[72]427                """
428                Display To and CC addresses in description field
429                """
[288]430                s = ''
[213]431                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
432                if m['To'] and len(m['To']) > 0:
[288]433                        s = "'''To:''' %s\r\n" %(m['To'])
[22]434                if m['Cc'] and len(m['Cc']) > 0:
[288]435                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]436
[288]437                return  self.email_to_unicode(s)
[22]438
[138]439
[194]440        def get_sender_info(self, message):
[45]441                """
[72]442                Get the default author name and email address from the message
[226]443                """
[43]444
[226]445                self.email_to = self.email_to_unicode(message['to'])
446                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
447
[194]448                self.email_from = self.email_to_unicode(message['from'])
[287]449                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]450
[304]451                ## Trac can not handle author's name that contains spaces
452                #  and forbid the ticket email address as author field
[194]453
[305]454                if self.email_addr == self.trac_smtp_from:
[304]455                        self.author = "email2trac"
456                else:
457                        self.author = self.email_addr
458
[194]459                if self.IGNORE_TRAC_USER_SETTINGS:
460                        return
461
462                # Is this a registered user, use email address as search key:
463                # result:
464                #   u : login name
465                #   n : Name that the user has set in the settings tab
466                #   e : email address that the user has set in the settings tab
[45]467                #
[194]468                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]469                        if e and (e.lower() == self.email_addr.lower()) ]
[43]470
[45]471                if len(users) == 1:
[194]472                        self.email_from = users[0][0]
[250]473                        self.author = users[0][0]
[45]474
[72]475        def set_reply_fields(self, ticket, message):
476                """
477                Set all the right fields for a new ticket
478                """
[299]479                if self.DEBUG:
480                        print 'TD: set_reply_fields'
[72]481
[270]482                ## Only use name or email adress
483                #ticket['reporter'] = self.email_from
484                ticket['reporter'] = self.author
485
486
[45]487                # Put all CC-addresses in ticket CC field
[43]488                #
489                if self.REPLY_ALL:
490
[299]491                        email_cc = ''
492
493                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
494
495                        if not cc_addrs:
[105]496                                return
[43]497
[299]498                        ## Build a list of forbidden CC addresses
499                        #
[300]500                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
501                        #to_list = list()
502                        #for n,e in to_addrs:
503                        #       to_list.append(e)
[299]504                               
[43]505                        # Remove reporter email address if notification is
506                        # on
507                        #
508                        if self.notification:
509                                try:
[299]510                                        cc_addrs.remove((self.author, self.email_addr))
[43]511                                except ValueError, detail:
512                                        pass
513
[299]514                        for name,addr in cc_addrs:
515               
516                                ## Prevent mail loop
517                                #
[300]518                                #if addr in to_list:
[304]519
520                                if addr == self.trac_smtp_from:
[299]521                                        if self.DEBUG:
522                                                print "Skipping %s mail address for CC-field" %(addr)
523                                        continue
[43]524
[299]525                                if email_cc:
526                                        email_cc = '%s, %s' %(email_cc, addr)
527                                else:
528                                        email_cc = addr
[96]529
[299]530                        if email_cc:
531                                if self.DEBUG:
532                                        print 'TD: set_reply_fields: %s' %email_cc
533
534                                ticket['cc'] = self.email_to_unicode(email_cc)
535
[96]536        def save_email_for_debug(self, message, tempfile=False):
[309]537
[96]538                if tempfile:
539                        import tempfile
540                        msg_file = tempfile.mktemp('.email2trac')
541                else:
[173]542                        #msg_file = '/var/tmp/msg.txt'
543                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
544
[44]545                print 'TD: saving email to %s' % msg_file
546                fx = open(msg_file, 'wb')
547                fx.write('%s' % message)
548                fx.close()
549                try:
550                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
551                except OSError:
552                        pass
553
[309]554                message_parts = self.get_message_parts(message)
555                message_parts = self.unique_attachment_names(message_parts)
556                body_text = self.body_text(message_parts)
557                self.debug_body(body_text, True)
558                self.debug_attachments(message_parts)
559
[288]560        def str_to_dict(self, s):
[164]561                """
[288]562                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]563                """
564
[297]565                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
[262]566
[164]567                result = dict()
568                for field in fields:
569                        try:
[262]570                                index, value = string.split(field, '=')
[169]571
572                                # We can not change the description of a ticket via the subject
573                                # line. The description is the body of the email
574                                #
575                                if index.lower() in ['description']:
576                                        continue
577
[164]578                                if value:
[165]579                                        result[index.lower()] = value
[169]580
[164]581                        except ValueError:
582                                pass
[165]583                return result
[167]584
[202]585        def update_ticket_fields(self, ticket, user_dict, use_default=None):
586                """
587                This will update the ticket fields. It will check if the
588                given fields are known and if the right values are specified
589                It will only update the ticket field value:
[169]590                        - If the field is known
[202]591                        - If the value supplied is valid for the ticket field.
592                          If not then there are two options:
593                           1) Skip the value (use_default=None)
594                           2) Set default value for field (use_default=1)
[169]595                """
[301]596                if self.DEBUG:
597                        print "TD: update_ticket_fields"
[169]598
599                # Build a system dictionary from the ticket fields
600                # with field as index and option as value
601                #
602                sys_dict = dict()
603                for field in ticket.fields:
[167]604                        try:
[169]605                                sys_dict[field['name']] = field['options']
606
[167]607                        except KeyError:
[169]608                                sys_dict[field['name']] = None
[167]609                                pass
[169]610
[301]611                ## Check user supplied fields an compare them with the
[169]612                # system one's
613                #
614                for field,value in user_dict.items():
[202]615                        if self.DEBUG >= 10:
616                                print  'user_field\t %s = %s' %(field,value)
[169]617
[301]618                        ## To prevent mail loop
619                        #
620                        if field == 'cc':
621
622                                cc_list = user_dict['cc'].split(',')
623
[304]624                                if self.trac_smtp_from in cc_list:
[301]625                                        if self.DEBUG > 10:
[304]626                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
627                                        cc_list.remove(self.trac_smtp_from)
[301]628
629                                value = ','.join(cc_list)
630                               
631
[169]632                        if sys_dict.has_key(field):
633
634                                # Check if value is an allowed system option, if TypeError then
635                                # every value is allowed
636                                #
637                                try:
638                                        if value in sys_dict[field]:
639                                                ticket[field] = value
[202]640                                        else:
641                                                # Must we set a default if value is not allowed
642                                                #
643                                                if use_default:
644                                                        value = self.get_config('ticket', 'default_%s' %(field) )
645                                                        ticket[field] = value
[169]646
647                                except TypeError:
648                                        ticket[field] = value
[202]649
650                                if self.DEBUG >= 10:
651                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]652                                       
[260]653        def ticket_update(self, m, id, spam):
[78]654                """
[79]655                If the current email is a reply to an existing ticket, this function
656                will append the contents of this email to that ticket, instead of
657                creating a new one.
[78]658                """
[250]659                if self.DEBUG:
[260]660                        print "TD: ticket_update: %s" %id
[202]661
[164]662                # Must we update ticket fields
663                #
[220]664                update_fields = dict()
[165]665                try:
[260]666                        id, keywords = string.split(id, '?')
[262]667
668                        # Skip the last ':' character
669                        #
670                        keywords = keywords[:-1]
[220]671                        update_fields = self.str_to_dict(keywords)
[165]672
673                        # Strip '#'
674                        #
[260]675                        self.id = int(id[1:])
[165]676
[260]677                except ValueError:
[165]678                        # Strip '#' and ':'
679                        #
[260]680                        self.id = int(id[1:-1])
[164]681
[71]682
[194]683                # When is the change committed
684                #
685                if self.VERSION == 0.11:
686                        utc = UTC()
687                        when = datetime.now(utc)
688                else:
689                        when = int(time.time())
[77]690
[172]691                try:
[253]692                        tkt = Ticket(self.env, self.id, self.db)
[172]693                except util.TracError, detail:
[253]694                        # Not a valid ticket
695                        self.id = None
[172]696                        return False
[126]697
[288]698                # How many changes has this ticket
699                cnum = len(tkt.get_changelog())
700
701
[220]702                # reopen the ticket if it is was closed
703                # We must use the ticket workflow framework
704                #
705                if tkt['status'] in ['closed']:
706
[257]707                        #print controller.actions['reopen']
708                        #
709                        # As reference 
710                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
711                        #
712                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
713                        #print 'controller : ', a
714                        #
715                        #b = controller.get_all_status()
716                        #print 'get all status: ', b
717                        #
718                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
719                        #print 'get_ticket_changes :', b
720
[288]721                        if self.WORKFLOW and (self.VERSION in [0.11]) :
[257]722                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
723                                from trac.test import Mock, MockPerm
724
725                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
726
727                                controller = ConfigurableTicketWorkflow(self.env)
728                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
729
730                                if self.DEBUG:
731                                        print 'TD: Workflow ticket update fields: ', fields
732
733                                for key in fields.keys():
734                                        tkt[key] = fields[key]
735
736                        else:
737                                tkt['status'] = 'reopened'
738                                tkt['resolution'] = ''
739
[309]740                # Must we update some ticket fields properties via subjectline
[172]741                #
[220]742                if update_fields:
743                        self.update_ticket_fields(tkt, update_fields)
[166]744
[236]745                message_parts = self.get_message_parts(m)
[253]746                message_parts = self.unique_attachment_names(message_parts)
[210]747
[309]748                # Must we update some ticket fields properties via body_text
749                #
750                if self.properties:
751                                self.update_ticket_fields(tkt, self.properties)
752
[177]753                if self.EMAIL_HEADER:
[236]754                        message_parts.insert(0, self.email_header_txt(m))
[76]755
[236]756                body_text = self.body_text(message_parts)
757
[309]758                if body_text.strip() or update_fields or self.properties:
[250]759                        if self.DRY_RUN:
[288]760                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]761                        else:
[288]762                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
763                       
[219]764
[129]765                if self.VERSION  == 0.9:
[288]766                        s = self.attachments(message_parts, True)
[129]767                else:
[288]768                        s = self.attachments(message_parts)
[76]769
[204]770                if self.notification and not spam:
[253]771                        self.notify(tkt, False, when)
[72]772
[71]773                return True
774
[202]775        def set_ticket_fields(self, ticket):
[77]776                """
[202]777                set the ticket fields to value specified
778                        - /etc/email2trac.conf with <prefix>_<field>
779                        - trac default values, trac.ini
780                """
781                user_dict = dict()
782
783                for field in ticket.fields:
784
785                        name = field['name']
786
[215]787                        # skip some fields like resolution
788                        #
789                        if name in [ 'resolution' ]:
790                                continue
791
[202]792                        # default trac value
793                        #
[233]794                        if not field.get('custom'):
795                                value = self.get_config('ticket', 'default_%s' %(name) )
796                        else:
797                                value = field.get('value')
798                                options = field.get('options')
[234]799                                if value and options and value not in options:
[233]800                                        value = options[int(value)]
801
[202]802                        if self.DEBUG > 10:
803                                print 'trac.ini name %s = %s' %(name, value)
804
[206]805                        prefix = self.parameters['ticket_prefix']
[202]806                        try:
[206]807                                value = self.parameters['%s_%s' %(prefix, name)]
[202]808                                if self.DEBUG > 10:
809                                        print 'email2trac.conf %s = %s ' %(name, value)
810
811                        except KeyError, detail:
812                                pass
813               
814                        if self.DEBUG:
815                                print 'user_dict[%s] = %s' %(name, value)
816
817                        user_dict[name] = value
818
819                self.update_ticket_fields(ticket, user_dict, use_default=1)
820
821                # Set status ticket
822                #`
823                ticket['status'] = 'new'
824
825
826
[262]827        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]828                """
[77]829                Create a new ticket
830                """
[250]831                if self.DEBUG:
832                        print "TD: new_ticket"
833
[41]834                tkt = Ticket(self.env)
[202]835                self.set_ticket_fields(tkt)
836
837                # Old style setting for component, will be removed
838                #
[204]839                if spam:
840                        tkt['component'] = 'Spam'
841
[206]842                elif self.parameters.has_key('component'):
843                        tkt['component'] = self.parameters['component']
[201]844
[22]845                if not msg['Subject']:
[151]846                        tkt['summary'] = u'(No subject)'
[22]847                else:
[264]848                        tkt['summary'] = subject
[22]849
[72]850                self.set_reply_fields(tkt, msg)
[22]851
[262]852                if set_fields:
853                        rest, keywords = string.split(set_fields, '?')
854
855                        if keywords:
856                                update_fields = self.str_to_dict(keywords)
857                                self.update_ticket_fields(tkt, update_fields)
858
[45]859                # produce e-mail like header
860                #
[22]861                head = ''
862                if self.EMAIL_HEADER > 0:
863                        head = self.email_header_txt(msg)
[296]864
[236]865                message_parts = self.get_message_parts(msg)
[309]866
867                # Must we update some ticket fields properties via body_text
868                #
869                if self.properties:
870                                self.update_ticket_fields(tkt, self.properties)
871
[296]872                if self.DEBUG:
873                        print 'TD: self.get_message_parts ',
874                        print message_parts
875
[236]876                message_parts = self.unique_attachment_names(message_parts)
[296]877                if self.DEBUG:
878                        print 'TD: self.unique_attachment_names',
879                        print message_parts
[236]880               
881                if self.EMAIL_HEADER > 0:
882                        message_parts.insert(0, self.email_header_txt(msg))
883                       
884                body_text = self.body_text(message_parts)
[45]885
[236]886                tkt['description'] = body_text
[90]887
[182]888                #when = int(time.time())
[192]889                #
[182]890                utc = UTC()
891                when = datetime.now(utc)
[45]892
[253]893                if not self.DRY_RUN:
894                        self.id = tkt.insert()
[273]895       
[90]896                changed = False
897                comment = ''
[77]898
[273]899                # some routines in trac are dependend on ticket id     
900                # like alternate notify template
901                #
902                if self.notify_template:
[274]903                        tkt['id'] = self.id
[273]904                        changed = True
905
[295]906                ## Rewrite the description if we have mailto enabled
[45]907                #
[72]908                if self.MAILTO:
[100]909                        changed = True
[142]910                        comment = u'\nadded mailto line\n'
[253]911                        mailto = self.html_mailto_link( m['Subject'], body_text)
912
[213]913                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]914                                %(head, mailto, body_text)
[295]915       
916                ## Save the attachments to the ticket   
917                #
918                has_attachments =  self.attachments(message_parts)
919
[292]920                #  Disabled
921                #if has_attachments:
922                #       changed = True
923                #       comment = '%s\n%s\n' %(comment, has_attachments)
[45]924
[90]925                if changed:
[204]926                        if self.DRY_RUN:
[250]927                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
[201]928                        else:
929                                tkt.save_changes(self.author, comment)
930                                #print tkt.get_changelog(self.db, when)
[90]931
[250]932                if self.notification and not spam:
[253]933                        self.notify(tkt, True)
[45]934
[260]935
936        def blog(self, id):
937                """
938                The blog create/update function
939                """
940                # import the modules
941                #
942                from tracfullblog.core import FullBlogCore
943                from tracfullblog.model import BlogPost, BlogCommen
944
945                # instantiate blog core
946                blog = FullBlogCore(self.env)
947                req = ''
948               
949                if id:
950
951                        # update blog
952                        #
[268]953                        comment = BlogComment(self.env, id)
[260]954                        comment.author = self.author
955                        comment.comment = self.get_body_text(m)
956                        blog.create_comment(req, comment)
957
958                else:
959                        # create blog
960                        #
961                        import time
962                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
963
964                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
965                       
966                        post.author = self.author
967                        post.title = self.email_to_unicode(m['Subject'])
968                        post.body = self.get_body_text(m)
969                       
970                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
971
[309]972        def save_message_for_debug(self):
973                """
974                Save the messages. So we can use it for debug purposes
975                """
976               
[260]977
[77]978        def parse(self, fp):
[96]979                global m
980
[77]981                m = email.message_from_file(fp)
[239]982               
[77]983                if not m:
[221]984                        if self.DEBUG:
[250]985                                print "TD: This is not a valid email message format"
[77]986                        return
[239]987                       
988                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
989                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
990
[77]991                if self.DEBUG > 1:        # save the entire e-mail message text
[219]992                        self.save_email_for_debug(m, True)
[77]993
994                self.db = self.env.get_db_cnx()
[194]995                self.get_sender_info(m)
[152]996
[221]997                if not self.email_header_acl('white_list', self.email_addr, True):
998                        if self.DEBUG > 1 :
999                                print 'Message rejected : %s not in white list' %(self.email_addr)
1000                        return False
[77]1001
[221]1002                if self.email_header_acl('black_list', self.email_addr, False):
1003                        if self.DEBUG > 1 :
1004                                print 'Message rejected : %s in black list' %(self.email_addr)
1005                        return False
1006
[227]1007                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]1008                        if self.DEBUG > 1 :
1009                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1010                        return False
1011
[204]1012                # If drop the message
[194]1013                #
[204]1014                if self.spam(m) == 'drop':
[194]1015                        return False
1016
[204]1017                elif self.spam(m) == 'spam':
1018                        spam_msg = True
1019                else:
1020                        spam_msg = False
1021
[77]1022                if self.get_config('notification', 'smtp_enabled') in ['true']:
1023                        self.notification = 1
1024                else:
1025                        self.notification = 0
1026
[304]1027
[260]1028                # Check if  FullBlogPlugin is installed
[77]1029                #
[260]1030                blog_enabled = None
1031                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1032                        blog_enabled = True
1033                       
1034                if not m['Subject']:
1035                        return False
1036                else:
1037                        subject  = self.email_to_unicode(m['Subject'])         
[77]1038
[260]1039                #
1040                # [hic] #1529: Re: LRZ
1041                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1042                #
1043                TICKET_RE = re.compile(r"""
1044                        (?P<blog>blog:(?P<blog_id>\w*))
[262]1045                        |(?P<new_fields>[#][?].*)
1046                        |(?P<reply>[#][\d]+:)
1047                        |(?P<reply_fields>[#][\d]+\?.*?:)
[260]1048                        """, re.VERBOSE)
[77]1049
[265]1050                # Find out if this is a ticket or a blog
1051                #
[260]1052                result =  TICKET_RE.search(subject)
1053
1054                if result:
[309]1055                        if result.group('blog'):
1056                                if blog_enabled:
1057                                        self.blog(result.group('blog_id'))
1058                                else:
1059                                        if self.DEBUG:
1060                                                print "Fullblog plugin is not installed"
1061                                                return
[260]1062
1063                        # update ticket + fields
1064                        #
[262]1065                        if result.group('reply_fields') and self.TICKET_UPDATE:
1066                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
[260]1067
1068                        # Update ticket
1069                        #
[262]1070                        elif result.group('reply') and self.TICKET_UPDATE:
1071                                self.ticket_update(m, result.group('reply'), spam_msg)
[260]1072
[262]1073                        # New ticket + fields
1074                        #
1075                        elif result.group('new_fields'):
1076                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1077
[260]1078                # Create ticket
1079                #
1080                else:
[262]1081                        self.new_ticket(m, subject, spam_msg)
[260]1082
[136]1083        def strip_signature(self, text):
1084                """
1085                Strip signature from message, inspired by Mailman software
1086                """
1087                body = []
1088                for line in text.splitlines():
1089                        if line == '-- ':
1090                                break
1091                        body.append(line)
1092
1093                return ('\n'.join(body))
1094
[231]1095        def reflow(self, text, delsp = 0):
1096                """
1097                Reflow the message based on the format="flowed" specification (RFC 3676)
1098                """
1099                flowedlines = []
1100                quotelevel = 0
1101                prevflowed = 0
1102
1103                for line in text.splitlines():
1104                        from re import match
1105                       
1106                        # Figure out the quote level and the content of the current line
1107                        m = match('(>*)( ?)(.*)', line)
1108                        linequotelevel = len(m.group(1))
1109                        line = m.group(3)
1110
1111                        # Determine whether this line is flowed
1112                        if line and line != '-- ' and line[-1] == ' ':
1113                                flowed = 1
1114                        else:
1115                                flowed = 0
1116
1117                        if flowed and delsp and line and line[-1] == ' ':
1118                                line = line[:-1]
1119
1120                        # If the previous line is flowed, append this line to it
1121                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1122                                flowedlines[-1] += line
1123                        # Otherwise, start a new line
1124                        else:
1125                                flowedlines.append('>' * linequotelevel + line)
1126
1127                        prevflowed = flowed
1128                       
1129
1130                return '\n'.join(flowedlines)
1131
[191]1132        def strip_quotes(self, text):
[193]1133                """
1134                Strip quotes from message by Nicolas Mendoza
1135                """
1136                body = []
1137                for line in text.splitlines():
1138                        if line.startswith(self.EMAIL_QUOTE):
1139                                continue
1140                        body.append(line)
[151]1141
[193]1142                return ('\n'.join(body))
[191]1143
[309]1144        def inline_properties(self, text):
1145                """
1146                Parse text if we use inline keywords to set ticket fields
1147                """
1148                if self.DEBUG:
1149                        print 'TD: inline_properties function'
1150
1151                properties = dict()
1152                body = list()
1153
1154                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1155
1156                for line in text.splitlines():
1157                        match = INLINE_EXP.match(line)
1158                        if match:
1159                                print match.groups()
1160                                keyword, value = match.groups()
1161                                self.properties[keyword] = value.strip()
1162                        else:
1163                                body.append(line)
1164                               
1165                return '\n'.join(body)
1166
1167
[154]1168        def wrap_text(self, text, replace_whitespace = False):
[151]1169                """
[191]1170                Will break a lines longer then given length into several small
1171                lines of size given length
[151]1172                """
1173                import textwrap
[154]1174
[151]1175                LINESEPARATOR = '\n'
[153]1176                reformat = ''
[151]1177
[154]1178                for s in text.split(LINESEPARATOR):
1179                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1180                        if tmp:
1181                                reformat = '%s\n%s' %(reformat,tmp)
1182                        else:
1183                                reformat = '%s\n' %reformat
[153]1184
1185                return reformat
1186
[154]1187                # Python2.4 and higher
1188                #
1189                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1190                #
1191
1192
[236]1193        def get_message_parts(self, msg):
[45]1194                """
[236]1195                parses the email message and returns a list of body parts and attachments
1196                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1197                """
[309]1198                message_parts = list()
[294]1199       
[278]1200                ALTERNATIVE_MULTIPART = False
1201
[22]1202                for part in msg.walk():
[236]1203                        if self.DEBUG:
[278]1204                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
[236]1205                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[278]1206
1207                        ## Check content type
[294]1208                        #
1209                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1210
[294]1211                                if self.DEBUG:
1212                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
[238]1213
1214                                continue
1215
[294]1216                        ## Catch some mulitpart execptions
1217                        #
1218                        if part.get_content_type() == 'multipart/alternative':
[278]1219                                ALTERNATIVE_MULTIPART = True
1220                                continue
1221
[294]1222                        ## Skip multipart containers
[278]1223                        #
[45]1224                        if part.get_content_maintype() == 'multipart':
[278]1225                                if self.DEBUG:
1226                                        print "TD: Skipping multipart container"
[22]1227                                continue
[278]1228                       
[294]1229                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1230                        #
[236]1231                        inline = self.inline_part(part)
1232
[294]1233                        ## Drop HTML message
1234                        #
[278]1235                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1236                                if part.get_content_type() == 'text/html':
1237                                        if self.DEBUG:
1238                                                print "TD: Skipping alternative HTML message"
1239
1240                                        ALTERNATIVE_MULTIPART = False
1241                                        continue
1242
[294]1243                        ## Inline text parts are where the body is
1244                        #
[236]1245                        if part.get_content_type() == 'text/plain' and inline:
1246                                if self.DEBUG:
1247                                        print 'TD:               Inline body part'
1248
[45]1249                                # Try to decode, if fails then do not decode
1250                                #
[90]1251                                body_text = part.get_payload(decode=1)
[45]1252                                if not body_text:                       
[90]1253                                        body_text = part.get_payload(decode=0)
[231]1254
[232]1255                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1256                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1257
1258                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1259                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1260       
[136]1261                                if self.STRIP_SIGNATURE:
1262                                        body_text = self.strip_signature(body_text)
[22]1263
[191]1264                                if self.STRIP_QUOTES:
1265                                        body_text = self.strip_quotes(body_text)
1266
[309]1267                                if self.INLINE_PROPERTIES:
1268                                        body_text = self.inline_properties(body_text)
1269
[148]1270                                if self.USE_TEXTWRAP:
[151]1271                                        body_text = self.wrap_text(body_text)
[148]1272
[294]1273                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1274                                #
[100]1275                                charset = part.get_content_charset()
[102]1276                                if not charset:
1277                                        charset = 'iso-8859-15'
1278
[89]1279                                try:
[96]1280                                        ubody_text = unicode(body_text, charset)
[100]1281
1282                                except UnicodeError, detail:
[96]1283                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1284
[100]1285                                except LookupError, detail:
[139]1286                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1287
[236]1288                                if self.VERBATIM_FORMAT:
1289                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1290                                else:
1291                                        message_parts.append('%s' %ubody_text)
1292                        else:
1293                                if self.DEBUG:
1294                                        print 'TD:               Filename: %s' % part.get_filename()
[22]1295
[236]1296                                message_parts.append((part.get_filename(), part))
1297
1298                return message_parts
1299               
[253]1300        def unique_attachment_names(self, message_parts):
[296]1301                """
1302                """
[236]1303                renamed_parts = []
1304                attachment_names = set()
[296]1305
[236]1306                for part in message_parts:
1307                       
[296]1308                        ## If not an attachment, leave it alone
1309                        #
[236]1310                        if not isinstance(part, tuple):
1311                                renamed_parts.append(part)
1312                                continue
1313                               
1314                        (filename, part) = part
[295]1315
[296]1316                        ## If no filename, use a default one
1317                        #
1318                        if not filename:
[236]1319                                filename = 'untitled-part'
[22]1320
[242]1321                                # Guess the extension from the content type, use non strict mode
1322                                # some additional non-standard but commonly used MIME types
1323                                # are also recognized
1324                                #
1325                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1326                                if not ext:
1327                                        ext = '.bin'
[22]1328
[236]1329                                filename = '%s%s' % (filename, ext)
[22]1330
[296]1331# We now use the attachment insert function
1332#
1333                        ## Discard relative paths in attachment names
1334                        #
1335                        #filename = filename.replace('\\', '/').replace(':', '/')
1336                        #filename = os.path.basename(filename)
1337                        #
[236]1338                        # We try to normalize the filename to utf-8 NFC if we can.
1339                        # Files uploaded from OS X might be in NFD.
1340                        # Check python version and then try it
1341                        #
[296]1342                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1343                        #       try:
1344                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1345                        #       except TypeError:
1346                        #               pass
[100]1347
[236]1348                        # Make the filename unique for this ticket
1349                        num = 0
1350                        unique_filename = filename
[296]1351                        dummy_filename, ext = os.path.splitext(filename)
[134]1352
[253]1353                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
[236]1354                                num += 1
[296]1355                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1356                               
1357                        if self.DEBUG:
1358                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[100]1359
[236]1360                        attachment_names.add(unique_filename)
1361
1362                        renamed_parts.append((filename, unique_filename, part))
[296]1363       
[236]1364                return renamed_parts
1365                       
1366        def inline_part(self, part):
1367                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1368               
1369                       
[253]1370        def attachment_exists(self, filename):
[250]1371
1372                if self.DEBUG:
[253]1373                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
[250]1374
1375                # We have no valid ticket id
1376                #
[253]1377                if not self.id:
[236]1378                        return False
[250]1379
[236]1380                try:
[253]1381                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
[236]1382                        return True
[250]1383                except attachment.ResourceNotFound:
[236]1384                        return False
1385                       
1386        def body_text(self, message_parts):
1387                body_text = []
1388               
1389                for part in message_parts:
1390                        # Plain text part, append it
1391                        if not isinstance(part, tuple):
1392                                body_text.extend(part.strip().splitlines())
1393                                body_text.append("")
1394                                continue
1395                               
1396                        (original, filename, part) = part
1397                        inline = self.inline_part(part)
1398                       
1399                        if part.get_content_maintype() == 'image' and inline:
1400                                body_text.append('[[Image(%s)]]' % filename)
1401                                body_text.append("")
1402                        else:
1403                                body_text.append('[attachment:"%s"]' % filename)
1404                                body_text.append("")
1405                               
1406                body_text = '\r\n'.join(body_text)
1407                return body_text
1408
[253]1409        def notify(self, tkt, new=True, modtime=0):
[79]1410                """
1411                A wrapper for the TRAC notify function. So we can use templates
1412                """
[250]1413                if self.DRY_RUN:
1414                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1415                                return
[41]1416                try:
1417                        # create false {abs_}href properties, to trick Notify()
1418                        #
[193]1419                        if not self.VERSION == 0.11:
[192]1420                                self.env.abs_href = Href(self.get_config('project', 'url'))
1421                                self.env.href = Href(self.get_config('project', 'url'))
[22]1422
[41]1423                        tn = TicketNotifyEmail(self.env)
[213]1424
[42]1425                        if self.notify_template:
[222]1426
[221]1427                                if self.VERSION == 0.11:
[222]1428
[221]1429                                        from trac.web.chrome import Chrome
[222]1430
1431                                        if self.notify_template_update and not new:
1432                                                tn.template_name = self.notify_template_update
1433                                        else:
1434                                                tn.template_name = self.notify_template
1435
[221]1436                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1437                                               
1438                                else:
[222]1439
[221]1440                                        tn.template_name = self.notify_template;
[42]1441
[77]1442                        tn.notify(tkt, new, modtime)
[41]1443
1444                except Exception, e:
[253]1445                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1446
[253]1447        def html_mailto_link(self, subject, body):
1448                """
1449                This function returns a HTML mailto tag with the ticket id and author email address
1450                """
[72]1451                if not self.author:
[143]1452                        author = self.email_addr
[22]1453                else:   
[142]1454                        author = self.author
[22]1455
[253]1456                # use urllib to escape the chars
[22]1457                #
[288]1458                s = 'mailto:%s?Subject=%s&Cc=%s' %(
[74]1459                       urllib.quote(self.email_addr),
[253]1460                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[74]1461                           urllib.quote(self.MAILTO_CC)
1462                           )
1463
[290]1464                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
[288]1465                return s
[22]1466
[253]1467        def attachments(self, message_parts, update=False):
[79]1468                '''
1469                save any attachments as files in the ticket's directory
1470                '''
[237]1471                if self.DRY_RUN:
[250]1472                        print "DRY_RUN: no attachments saved"
[237]1473                        return ''
1474
[22]1475                count = 0
[152]1476
1477                # Get Maxium attachment size
1478                #
1479                max_size = int(self.get_config('attachment', 'max_size'))
[153]1480                status   = ''
[236]1481               
1482                for part in message_parts:
1483                        # Skip body parts
1484                        if not isinstance(part, tuple):
[22]1485                                continue
[236]1486                               
1487                        (original, filename, part) = part
[48]1488                        #
[172]1489                        # Must be tuneables HvB
1490                        #
[236]1491                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1492                        text = part.get_payload(decode=1)
1493                        if not text:
1494                                text = '(None)'
[48]1495                        fd.write(text)
1496                        fd.close()
[22]1497
[153]1498                        # get the file_size
[22]1499                        #
[48]1500                        stats = os.lstat(path)
[153]1501                        file_size = stats[stat.ST_SIZE]
[22]1502
[152]1503                        # Check if the attachment size is allowed
1504                        #
[153]1505                        if (max_size != -1) and (file_size > max_size):
1506                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1507                                        %(status, original, file_size, max_size)
[152]1508
1509                                os.unlink(path)
1510                                continue
1511                        else:
1512                                count = count + 1
1513                                       
[172]1514                        # Insert the attachment
[73]1515                        #
[242]1516                        fd = open(path, 'rb')
[253]1517                        att = attachment.Attachment(self.env, 'ticket', self.id)
[73]1518
[172]1519                        # This will break the ticket_update system, the body_text is vaporized
1520                        # ;-(
1521                        #
1522                        if not update:
1523                                att.author = self.author
1524                                att.description = self.email_to_unicode('Added by email2trac')
[73]1525
[236]1526                        att.insert(filename, fd, file_size)
[296]1527
[172]1528                        #except  util.TracError, detail:
1529                        #       print detail
[73]1530
[103]1531                        # Remove the created temporary filename
1532                        #
[172]1533                        fd.close()
[103]1534                        os.unlink(path)
1535
[77]1536                # Return how many attachments
1537                #
[153]1538                status = 'This message has %d attachment(s)\n%s' %(count, status)
1539                return status
[22]1540
[77]1541
[22]1542def mkdir_p(dir, mode):
1543        '''do a mkdir -p'''
1544
1545        arr = string.split(dir, '/')
1546        path = ''
1547        for part in arr:
1548                path = '%s/%s' % (path, part)
1549                try:
1550                        stats = os.stat(path)
1551                except OSError:
1552                        os.mkdir(path, mode)
1553
1554def ReadConfig(file, name):
1555        """
1556        Parse the config file
1557        """
1558        if not os.path.isfile(file):
[79]1559                print 'File %s does not exist' %file
[22]1560                sys.exit(1)
1561
[199]1562        config = trac_config.Configuration(file)
[22]1563
1564        # Use given project name else use defaults
1565        #
1566        if name:
[199]1567                sections = config.sections()
1568                if not name in sections:
[79]1569                        print "Not a valid project name: %s" %name
[199]1570                        print "Valid names: %s" %sections
[22]1571                        sys.exit(1)
1572
1573                project =  dict()
[199]1574                for option, value in  config.options(name):
1575                        project[option] = value
[22]1576
1577        else:
[270]1578                # use some trac internals to get the defaults
[217]1579                #
1580                project = config.parser.defaults()
[22]1581
1582        return project
1583
[87]1584
[22]1585if __name__ == '__main__':
1586        # Default config file
1587        #
[24]1588        configfile = '@email2trac_conf@'
[22]1589        project = ''
1590        component = ''
[202]1591        ticket_prefix = 'default'
[204]1592        dry_run = None
[202]1593
[87]1594        ENABLE_SYSLOG = 0
[201]1595
[204]1596
[202]1597        SHORT_OPT = 'chf:np:t:'
1598        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1599
[22]1600        try:
[201]1601                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1602        except getopt.error,detail:
1603                print __doc__
1604                print detail
1605                sys.exit(1)
[87]1606       
[22]1607        project_name = None
1608        for opt,value in opts:
1609                if opt in [ '-h', '--help']:
1610                        print __doc__
1611                        sys.exit(0)
1612                elif opt in ['-c', '--component']:
1613                        component = value
1614                elif opt in ['-f', '--file']:
1615                        configfile = value
[201]1616                elif opt in ['-n', '--dry-run']:
[204]1617                        dry_run = True
[22]1618                elif opt in ['-p', '--project']:
1619                        project_name = value
[202]1620                elif opt in ['-t', '--ticket_prefix']:
1621                        ticket_prefix = value
[87]1622       
[22]1623        settings = ReadConfig(configfile, project_name)
1624        if not settings.has_key('project'):
1625                print __doc__
[79]1626                print 'No Trac project is defined in the email2trac config file.'
[22]1627                sys.exit(1)
[87]1628       
[22]1629        if component:
1630                settings['component'] = component
[202]1631
1632        # The default prefix for ticket values in email2trac.conf
1633        #
1634        settings['ticket_prefix'] = ticket_prefix
[206]1635        settings['dry_run'] = dry_run
[87]1636       
[22]1637        if settings.has_key('trac_version'):
[189]1638                version = settings['trac_version']
[22]1639        else:
1640                version = trac_default_version
1641
[189]1642
[22]1643        #debug HvB
1644        #print settings
[189]1645
[87]1646        try:
[189]1647                if version == '0.9':
[87]1648                        from trac import attachment
1649                        from trac.env import Environment
1650                        from trac.ticket import Ticket
1651                        from trac.web.href import Href
1652                        from trac import util
1653                        from trac.Notify import TicketNotifyEmail
[189]1654                elif version == '0.10':
[87]1655                        from trac import attachment
1656                        from trac.env import Environment
1657                        from trac.ticket import Ticket
1658                        from trac.web.href import Href
1659                        from trac import util
[139]1660                        #
1661                        # return  util.text.to_unicode(str)
1662                        #
[87]1663                        # see http://projects.edgewall.com/trac/changeset/2799
1664                        from trac.ticket.notification import TicketNotifyEmail
[199]1665                        from trac import config as trac_config
[189]1666                elif version == '0.11':
[182]1667                        from trac import attachment
1668                        from trac.env import Environment
1669                        from trac.ticket import Ticket
1670                        from trac.web.href import Href
[199]1671                        from trac import config as trac_config
[182]1672                        from trac import util
[260]1673
1674
[182]1675                        #
1676                        # return  util.text.to_unicode(str)
1677                        #
1678                        # see http://projects.edgewall.com/trac/changeset/2799
1679                        from trac.ticket.notification import TicketNotifyEmail
[189]1680                else:
1681                        print 'TRAC version %s is not supported' %version
1682                        sys.exit(1)
1683                       
1684                if settings.has_key('enable_syslog'):
[190]1685                        if SYSLOG_AVAILABLE:
1686                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1687
[291]1688
1689                # Must be set before environment is created
1690                #
1691                if settings.has_key('python_egg_cache'):
1692                        python_egg_cache = str(settings['python_egg_cache'])
1693                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1694
[87]1695                env = Environment(settings['project'], create=0)
[206]1696                tktparser = TicketEmailParser(env, settings, float(version))
[87]1697                tktparser.parse(sys.stdin)
[22]1698
[87]1699        # Catch all errors ans log to SYSLOG if we have enabled this
1700        # else stdout
1701        #
1702        except Exception, error:
1703                if ENABLE_SYSLOG:
1704                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1705
[87]1706                        etype, evalue, etb = sys.exc_info()
1707                        for e in traceback.format_exception(etype, evalue, etb):
1708                                syslog.syslog(e)
[187]1709
[87]1710                        syslog.closelog()
1711                else:
1712                        traceback.print_exc()
[22]1713
[97]1714                if m:
[98]1715                        tktparser.save_email_for_debug(m, True)
[97]1716
[249]1717                sys.exit(1)
[22]1718# EOB
Note: See TracBrowser for help on using the repository browser.