source: trunk/email2trac.py.in @ 326

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

email2trac.py.in:

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