source: trunk/email2trac.py.in @ 319

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

When there a problems with saving attachments. Show it as comment in the ticket, closes #165

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