source: trunk/email2trac.py.in @ 317

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

email2trac.py.in:

  • A filename name can also containt unicode chars, so convert a filename name to unicode, see #177
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 42.8 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 317 2010-02-15 12:02:27Z 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)
[288]355                s = None
[22]356                for text,format in results:
357                        if format:
358                                try:
359                                        temp = unicode(text, format)
[139]360                                except UnicodeError, detail:
[22]361                                        # This always works
362                                        #
363                                        temp = unicode(text, 'iso-8859-15')
[139]364                                except LookupError, detail:
365                                        #text = 'ERROR: Could not find charset: %s, please install' %format
366                                        #temp = unicode(text, 'iso-8859-15')
367                                        temp = message_str
368                                       
[22]369                        else:
370                                temp = string.strip(text)
[92]371                                temp = unicode(text, 'iso-8859-15')
[22]372
[288]373                        if s:
374                                s = '%s %s' %(s, temp)
[22]375                        else:
[288]376                                s = '%s' %temp
[22]377
[288]378                #s = s.encode('utf-8')
379                return s
[22]380
[236]381
[22]382        def email_header_txt(self, m):
[72]383                """
384                Display To and CC addresses in description field
385                """
[288]386                s = ''
[213]387                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
388                if m['To'] and len(m['To']) > 0:
[288]389                        s = "'''To:''' %s\r\n" %(m['To'])
[22]390                if m['Cc'] and len(m['Cc']) > 0:
[288]391                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]392
[288]393                return  self.email_to_unicode(s)
[22]394
[138]395
[194]396        def get_sender_info(self, message):
[45]397                """
[72]398                Get the default author name and email address from the message
[226]399                """
[43]400
[226]401                self.email_to = self.email_to_unicode(message['to'])
402                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
403
[194]404                self.email_from = self.email_to_unicode(message['from'])
[287]405                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]406
[304]407                ## Trac can not handle author's name that contains spaces
408                #  and forbid the ticket email address as author field
[194]409
[305]410                if self.email_addr == self.trac_smtp_from:
[304]411                        self.author = "email2trac"
412                else:
413                        self.author = self.email_addr
414
[194]415                if self.IGNORE_TRAC_USER_SETTINGS:
416                        return
417
418                # Is this a registered user, use email address as search key:
419                # result:
420                #   u : login name
421                #   n : Name that the user has set in the settings tab
422                #   e : email address that the user has set in the settings tab
[45]423                #
[194]424                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]425                        if e and (e.lower() == self.email_addr.lower()) ]
[43]426
[45]427                if len(users) == 1:
[194]428                        self.email_from = users[0][0]
[250]429                        self.author = users[0][0]
[45]430
[72]431        def set_reply_fields(self, ticket, message):
432                """
433                Set all the right fields for a new ticket
434                """
[299]435                if self.DEBUG:
436                        print 'TD: set_reply_fields'
[72]437
[270]438                ## Only use name or email adress
439                #ticket['reporter'] = self.email_from
440                ticket['reporter'] = self.author
441
442
[45]443                # Put all CC-addresses in ticket CC field
[43]444                #
445                if self.REPLY_ALL:
446
[299]447                        email_cc = ''
448
449                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
450
451                        if not cc_addrs:
[105]452                                return
[43]453
[299]454                        ## Build a list of forbidden CC addresses
455                        #
[300]456                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
457                        #to_list = list()
458                        #for n,e in to_addrs:
459                        #       to_list.append(e)
[299]460                               
[43]461                        # Remove reporter email address if notification is
462                        # on
463                        #
464                        if self.notification:
465                                try:
[299]466                                        cc_addrs.remove((self.author, self.email_addr))
[43]467                                except ValueError, detail:
468                                        pass
469
[299]470                        for name,addr in cc_addrs:
471               
472                                ## Prevent mail loop
473                                #
[300]474                                #if addr in to_list:
[304]475
476                                if addr == self.trac_smtp_from:
[299]477                                        if self.DEBUG:
478                                                print "Skipping %s mail address for CC-field" %(addr)
479                                        continue
[43]480
[299]481                                if email_cc:
482                                        email_cc = '%s, %s' %(email_cc, addr)
483                                else:
484                                        email_cc = addr
[96]485
[299]486                        if email_cc:
487                                if self.DEBUG:
488                                        print 'TD: set_reply_fields: %s' %email_cc
489
490                                ticket['cc'] = self.email_to_unicode(email_cc)
491
[310]492        def debug_body(self, message_body, tempfile=False):
493                if tempfile:
494                        import tempfile
495                        body_file = tempfile.mktemp('.email2trac')
496                else:
497                        body_file = os.path.join(self.TMPDIR, 'body.txt')
498
499                print 'TD: writing body (%s)' % body_file
500                fx = open(body_file, 'wb')
501                if not message_body:
502                        message_body = '(None)'
503
504                message_body = message_body.encode('utf-8')
505                #message_body = unicode(message_body, 'iso-8859-15')
506
507                fx.write(message_body)
508                fx.close()
509                try:
510                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
511                except OSError:
512                        pass
513
514        def debug_attachments(self, message_parts):
[317]515                """
516                """
517                if self.VERBOSE:
518                        print "VB: debug_attachments"
519               
[310]520                n = 0
521                for part in message_parts:
522                        # Skip inline text parts
523                        if not isinstance(part, tuple):
524                                continue
525                               
526                        (original, filename, part) = part
527
528                        n = n + 1
529                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
530                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
531
532                        part_file = os.path.join(self.TMPDIR, filename)
533                        #part_file = '/var/tmp/part%d' % n
534                        print 'TD: writing part%d (%s)' % (n,part_file)
535                        fx = open(part_file, 'wb')
536                        text = part.get_payload(decode=1)
537                        if not text:
538                                text = '(None)'
539                        fx.write(text)
540                        fx.close()
541                        try:
542                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
543                        except OSError:
544                                pass
545
[96]546        def save_email_for_debug(self, message, tempfile=False):
[309]547
[96]548                if tempfile:
549                        import tempfile
550                        msg_file = tempfile.mktemp('.email2trac')
551                else:
[173]552                        #msg_file = '/var/tmp/msg.txt'
553                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
554
[44]555                print 'TD: saving email to %s' % msg_file
556                fx = open(msg_file, 'wb')
557                fx.write('%s' % message)
558                fx.close()
559                try:
560                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
561                except OSError:
562                        pass
563
[309]564                message_parts = self.get_message_parts(message)
565                message_parts = self.unique_attachment_names(message_parts)
566                body_text = self.body_text(message_parts)
567                self.debug_body(body_text, True)
568                self.debug_attachments(message_parts)
569
[288]570        def str_to_dict(self, s):
[164]571                """
[288]572                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]573                """
574
[297]575                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
[262]576
[164]577                result = dict()
578                for field in fields:
579                        try:
[262]580                                index, value = string.split(field, '=')
[169]581
582                                # We can not change the description of a ticket via the subject
583                                # line. The description is the body of the email
584                                #
585                                if index.lower() in ['description']:
586                                        continue
587
[164]588                                if value:
[165]589                                        result[index.lower()] = value
[169]590
[164]591                        except ValueError:
592                                pass
[165]593                return result
[167]594
[202]595        def update_ticket_fields(self, ticket, user_dict, use_default=None):
596                """
597                This will update the ticket fields. It will check if the
598                given fields are known and if the right values are specified
599                It will only update the ticket field value:
[169]600                        - If the field is known
[202]601                        - If the value supplied is valid for the ticket field.
602                          If not then there are two options:
603                           1) Skip the value (use_default=None)
604                           2) Set default value for field (use_default=1)
[169]605                """
[301]606                if self.DEBUG:
607                        print "TD: update_ticket_fields"
[169]608
609                # Build a system dictionary from the ticket fields
610                # with field as index and option as value
611                #
612                sys_dict = dict()
613                for field in ticket.fields:
[167]614                        try:
[169]615                                sys_dict[field['name']] = field['options']
616
[167]617                        except KeyError:
[169]618                                sys_dict[field['name']] = None
[167]619                                pass
[169]620
[301]621                ## Check user supplied fields an compare them with the
[169]622                # system one's
623                #
624                for field,value in user_dict.items():
[202]625                        if self.DEBUG >= 10:
626                                print  'user_field\t %s = %s' %(field,value)
[169]627
[301]628                        ## To prevent mail loop
629                        #
630                        if field == 'cc':
631
632                                cc_list = user_dict['cc'].split(',')
633
[304]634                                if self.trac_smtp_from in cc_list:
[301]635                                        if self.DEBUG > 10:
[304]636                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
637                                        cc_list.remove(self.trac_smtp_from)
[301]638
639                                value = ','.join(cc_list)
640                               
641
[169]642                        if sys_dict.has_key(field):
643
644                                # Check if value is an allowed system option, if TypeError then
645                                # every value is allowed
646                                #
647                                try:
648                                        if value in sys_dict[field]:
649                                                ticket[field] = value
[202]650                                        else:
651                                                # Must we set a default if value is not allowed
652                                                #
653                                                if use_default:
654                                                        value = self.get_config('ticket', 'default_%s' %(field) )
655                                                        ticket[field] = value
[169]656
657                                except TypeError:
658                                        ticket[field] = value
[202]659
660                                if self.DEBUG >= 10:
661                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]662                                       
[260]663        def ticket_update(self, m, id, spam):
[78]664                """
[79]665                If the current email is a reply to an existing ticket, this function
666                will append the contents of this email to that ticket, instead of
667                creating a new one.
[78]668                """
[250]669                if self.DEBUG:
[260]670                        print "TD: ticket_update: %s" %id
[202]671
[164]672                # Must we update ticket fields
673                #
[220]674                update_fields = dict()
[165]675                try:
[260]676                        id, keywords = string.split(id, '?')
[262]677
678                        # Skip the last ':' character
679                        #
680                        keywords = keywords[:-1]
[220]681                        update_fields = self.str_to_dict(keywords)
[165]682
683                        # Strip '#'
684                        #
[260]685                        self.id = int(id[1:])
[165]686
[260]687                except ValueError:
[165]688                        # Strip '#' and ':'
689                        #
[260]690                        self.id = int(id[1:-1])
[164]691
[71]692
[194]693                # When is the change committed
694                #
695                if self.VERSION == 0.11:
696                        utc = UTC()
697                        when = datetime.now(utc)
698                else:
699                        when = int(time.time())
[77]700
[172]701                try:
[253]702                        tkt = Ticket(self.env, self.id, self.db)
[172]703                except util.TracError, detail:
[253]704                        # Not a valid ticket
705                        self.id = None
[172]706                        return False
[126]707
[288]708                # How many changes has this ticket
709                cnum = len(tkt.get_changelog())
710
711
[220]712                # reopen the ticket if it is was closed
713                # We must use the ticket workflow framework
714                #
715                if tkt['status'] in ['closed']:
716
[257]717                        #print controller.actions['reopen']
718                        #
719                        # As reference 
720                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
721                        #
722                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
723                        #print 'controller : ', a
724                        #
725                        #b = controller.get_all_status()
726                        #print 'get all status: ', b
727                        #
728                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
729                        #print 'get_ticket_changes :', b
730
[288]731                        if self.WORKFLOW and (self.VERSION in [0.11]) :
[257]732                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
733                                from trac.test import Mock, MockPerm
734
735                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
736
737                                controller = ConfigurableTicketWorkflow(self.env)
738                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
739
740                                if self.DEBUG:
741                                        print 'TD: Workflow ticket update fields: ', fields
742
743                                for key in fields.keys():
744                                        tkt[key] = fields[key]
745
746                        else:
747                                tkt['status'] = 'reopened'
748                                tkt['resolution'] = ''
749
[309]750                # Must we update some ticket fields properties via subjectline
[172]751                #
[220]752                if update_fields:
753                        self.update_ticket_fields(tkt, update_fields)
[166]754
[236]755                message_parts = self.get_message_parts(m)
[253]756                message_parts = self.unique_attachment_names(message_parts)
[210]757
[309]758                # Must we update some ticket fields properties via body_text
759                #
760                if self.properties:
761                                self.update_ticket_fields(tkt, self.properties)
762
[177]763                if self.EMAIL_HEADER:
[236]764                        message_parts.insert(0, self.email_header_txt(m))
[76]765
[236]766                body_text = self.body_text(message_parts)
767
[309]768                if body_text.strip() or update_fields or self.properties:
[250]769                        if self.DRY_RUN:
[288]770                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]771                        else:
[288]772                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
773                       
[219]774
[129]775                if self.VERSION  == 0.9:
[288]776                        s = self.attachments(message_parts, True)
[129]777                else:
[288]778                        s = self.attachments(message_parts)
[76]779
[204]780                if self.notification and not spam:
[253]781                        self.notify(tkt, False, when)
[72]782
[71]783                return True
784
[202]785        def set_ticket_fields(self, ticket):
[77]786                """
[202]787                set the ticket fields to value specified
788                        - /etc/email2trac.conf with <prefix>_<field>
789                        - trac default values, trac.ini
790                """
791                user_dict = dict()
792
793                for field in ticket.fields:
794
795                        name = field['name']
796
[215]797                        # skip some fields like resolution
798                        #
799                        if name in [ 'resolution' ]:
800                                continue
801
[202]802                        # default trac value
803                        #
[233]804                        if not field.get('custom'):
805                                value = self.get_config('ticket', 'default_%s' %(name) )
806                        else:
807                                value = field.get('value')
808                                options = field.get('options')
[234]809                                if value and options and value not in options:
[233]810                                        value = options[int(value)]
811
[202]812                        if self.DEBUG > 10:
813                                print 'trac.ini name %s = %s' %(name, value)
814
[206]815                        prefix = self.parameters['ticket_prefix']
[202]816                        try:
[206]817                                value = self.parameters['%s_%s' %(prefix, name)]
[202]818                                if self.DEBUG > 10:
819                                        print 'email2trac.conf %s = %s ' %(name, value)
820
821                        except KeyError, detail:
822                                pass
823               
824                        if self.DEBUG:
825                                print 'user_dict[%s] = %s' %(name, value)
826
827                        user_dict[name] = value
828
829                self.update_ticket_fields(ticket, user_dict, use_default=1)
830
831                # Set status ticket
832                #`
833                ticket['status'] = 'new'
834
835
836
[262]837        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]838                """
[77]839                Create a new ticket
840                """
[250]841                if self.DEBUG:
842                        print "TD: new_ticket"
843
[41]844                tkt = Ticket(self.env)
[202]845                self.set_ticket_fields(tkt)
846
847                # Old style setting for component, will be removed
848                #
[204]849                if spam:
850                        tkt['component'] = 'Spam'
851
[206]852                elif self.parameters.has_key('component'):
853                        tkt['component'] = self.parameters['component']
[201]854
[22]855                if not msg['Subject']:
[151]856                        tkt['summary'] = u'(No subject)'
[22]857                else:
[264]858                        tkt['summary'] = subject
[22]859
[72]860                self.set_reply_fields(tkt, msg)
[22]861
[262]862                if set_fields:
863                        rest, keywords = string.split(set_fields, '?')
864
865                        if keywords:
866                                update_fields = self.str_to_dict(keywords)
867                                self.update_ticket_fields(tkt, update_fields)
868
[45]869                # produce e-mail like header
870                #
[22]871                head = ''
872                if self.EMAIL_HEADER > 0:
873                        head = self.email_header_txt(msg)
[296]874
[236]875                message_parts = self.get_message_parts(msg)
[309]876
877                # Must we update some ticket fields properties via body_text
878                #
879                if self.properties:
880                                self.update_ticket_fields(tkt, self.properties)
881
[296]882                if self.DEBUG:
883                        print 'TD: self.get_message_parts ',
884                        print message_parts
885
[236]886                message_parts = self.unique_attachment_names(message_parts)
[296]887                if self.DEBUG:
888                        print 'TD: self.unique_attachment_names',
889                        print message_parts
[236]890               
891                if self.EMAIL_HEADER > 0:
892                        message_parts.insert(0, self.email_header_txt(msg))
893                       
894                body_text = self.body_text(message_parts)
[45]895
[236]896                tkt['description'] = body_text
[90]897
[182]898                #when = int(time.time())
[192]899                #
[182]900                utc = UTC()
901                when = datetime.now(utc)
[45]902
[253]903                if not self.DRY_RUN:
904                        self.id = tkt.insert()
[273]905       
[90]906                changed = False
907                comment = ''
[77]908
[273]909                # some routines in trac are dependend on ticket id     
910                # like alternate notify template
911                #
912                if self.notify_template:
[274]913                        tkt['id'] = self.id
[273]914                        changed = True
915
[295]916                ## Rewrite the description if we have mailto enabled
[45]917                #
[72]918                if self.MAILTO:
[100]919                        changed = True
[142]920                        comment = u'\nadded mailto line\n'
[253]921                        mailto = self.html_mailto_link( m['Subject'], body_text)
922
[213]923                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]924                                %(head, mailto, body_text)
[295]925       
926                ## Save the attachments to the ticket   
927                #
928                has_attachments =  self.attachments(message_parts)
929
[292]930                #  Disabled
931                #if has_attachments:
932                #       changed = True
933                #       comment = '%s\n%s\n' %(comment, has_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'))
[153]1509                status   = ''
[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
[77]1565                # Return how many attachments
1566                #
[153]1567                status = 'This message has %d attachment(s)\n%s' %(count, status)
1568                return status
[22]1569
[77]1570
[22]1571def mkdir_p(dir, mode):
1572        '''do a mkdir -p'''
1573
1574        arr = string.split(dir, '/')
1575        path = ''
1576        for part in arr:
1577                path = '%s/%s' % (path, part)
1578                try:
1579                        stats = os.stat(path)
1580                except OSError:
1581                        os.mkdir(path, mode)
1582
1583def ReadConfig(file, name):
1584        """
1585        Parse the config file
1586        """
1587        if not os.path.isfile(file):
[79]1588                print 'File %s does not exist' %file
[22]1589                sys.exit(1)
1590
[199]1591        config = trac_config.Configuration(file)
[22]1592
1593        # Use given project name else use defaults
1594        #
1595        if name:
[199]1596                sections = config.sections()
1597                if not name in sections:
[79]1598                        print "Not a valid project name: %s" %name
[199]1599                        print "Valid names: %s" %sections
[22]1600                        sys.exit(1)
1601
1602                project =  dict()
[199]1603                for option, value in  config.options(name):
1604                        project[option] = value
[22]1605
1606        else:
[270]1607                # use some trac internals to get the defaults
[217]1608                #
1609                project = config.parser.defaults()
[22]1610
1611        return project
1612
[87]1613
[22]1614if __name__ == '__main__':
1615        # Default config file
1616        #
[24]1617        configfile = '@email2trac_conf@'
[22]1618        project = ''
1619        component = ''
[202]1620        ticket_prefix = 'default'
[204]1621        dry_run = None
[317]1622        verbose = None
[202]1623
[87]1624        ENABLE_SYSLOG = 0
[201]1625
[204]1626
[317]1627        SHORT_OPT = 'chf:np:t:v'
1628        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]1629
[22]1630        try:
[201]1631                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1632        except getopt.error,detail:
1633                print __doc__
1634                print detail
1635                sys.exit(1)
[87]1636       
[22]1637        project_name = None
1638        for opt,value in opts:
1639                if opt in [ '-h', '--help']:
1640                        print __doc__
1641                        sys.exit(0)
1642                elif opt in ['-c', '--component']:
1643                        component = value
1644                elif opt in ['-f', '--file']:
1645                        configfile = value
[201]1646                elif opt in ['-n', '--dry-run']:
[204]1647                        dry_run = True
[22]1648                elif opt in ['-p', '--project']:
1649                        project_name = value
[202]1650                elif opt in ['-t', '--ticket_prefix']:
1651                        ticket_prefix = value
[317]1652                elif opt in ['-v', '--version']:
1653                        verbose = True
[87]1654       
[22]1655        settings = ReadConfig(configfile, project_name)
1656        if not settings.has_key('project'):
1657                print __doc__
[79]1658                print 'No Trac project is defined in the email2trac config file.'
[22]1659                sys.exit(1)
[87]1660       
[22]1661        if component:
1662                settings['component'] = component
[202]1663
1664        # The default prefix for ticket values in email2trac.conf
1665        #
1666        settings['ticket_prefix'] = ticket_prefix
[206]1667        settings['dry_run'] = dry_run
[317]1668        settings['verbose'] = verbose
[87]1669       
[22]1670        if settings.has_key('trac_version'):
[189]1671                version = settings['trac_version']
[22]1672        else:
1673                version = trac_default_version
1674
[189]1675
[22]1676        #debug HvB
1677        #print settings
[189]1678
[87]1679        try:
[189]1680                if version == '0.9':
[87]1681                        from trac import attachment
1682                        from trac.env import Environment
1683                        from trac.ticket import Ticket
1684                        from trac.web.href import Href
1685                        from trac import util
1686                        from trac.Notify import TicketNotifyEmail
[189]1687                elif version == '0.10':
[87]1688                        from trac import attachment
1689                        from trac.env import Environment
1690                        from trac.ticket import Ticket
1691                        from trac.web.href import Href
1692                        from trac import util
[139]1693                        #
1694                        # return  util.text.to_unicode(str)
1695                        #
[87]1696                        # see http://projects.edgewall.com/trac/changeset/2799
1697                        from trac.ticket.notification import TicketNotifyEmail
[199]1698                        from trac import config as trac_config
[189]1699                elif version == '0.11':
[182]1700                        from trac import attachment
1701                        from trac.env import Environment
1702                        from trac.ticket import Ticket
1703                        from trac.web.href import Href
[199]1704                        from trac import config as trac_config
[182]1705                        from trac import util
[260]1706
1707
[182]1708                        #
1709                        # return  util.text.to_unicode(str)
1710                        #
1711                        # see http://projects.edgewall.com/trac/changeset/2799
1712                        from trac.ticket.notification import TicketNotifyEmail
[189]1713                else:
1714                        print 'TRAC version %s is not supported' %version
1715                        sys.exit(1)
1716                       
1717                if settings.has_key('enable_syslog'):
[190]1718                        if SYSLOG_AVAILABLE:
1719                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1720
[291]1721
1722                # Must be set before environment is created
1723                #
1724                if settings.has_key('python_egg_cache'):
1725                        python_egg_cache = str(settings['python_egg_cache'])
1726                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1727
[87]1728                env = Environment(settings['project'], create=0)
[206]1729                tktparser = TicketEmailParser(env, settings, float(version))
[87]1730                tktparser.parse(sys.stdin)
[22]1731
[87]1732        # Catch all errors ans log to SYSLOG if we have enabled this
1733        # else stdout
1734        #
1735        except Exception, error:
1736                if ENABLE_SYSLOG:
1737                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1738
[87]1739                        etype, evalue, etb = sys.exc_info()
1740                        for e in traceback.format_exception(etype, evalue, etb):
1741                                syslog.syslog(e)
[187]1742
[87]1743                        syslog.closelog()
1744                else:
1745                        traceback.print_exc()
[22]1746
[97]1747                if m:
[98]1748                        tktparser.save_email_for_debug(m, True)
[97]1749
[249]1750                sys.exit(1)
[22]1751# EOB
Note: See TracBrowser for help on using the repository browser.