source: trunk/email2trac.py.in @ 357

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

email2trac.py.in:

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