source: trunk/email2trac.py.in @ 335

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

Some more layout changes

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