source: trunk/email2trac.py.in @ 220

Last change on this file since 220 was 220, checked in by bas, 15 years ago

email2trac.py.in:

  • update_ticket_fields must be place after the re-opening a closed ticket
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 30.5 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
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
43                     => CC (Optional via reply_all option)
44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
55        reply_all    : 1               # OPTIONAL, if set then fill in ticket CC field
56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
60        trac_version : 0.9             # OPTIONAL, default is 0.10
61
62        [jouvin]                       # OPTIONAL project declaration, if set both fields necessary
63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h,--help
69                -f,--file  <configuration file>
70                -n,--dry-run
71                -p, --project <project name>
72                -t, --ticket_prefix <name>
73
74SVN Info:
75        $Id: email2trac.py.in 220 2008-10-09 14:28:02Z bas $
76"""
77import os
78import sys
79import string
80import getopt
81import stat
82import time
83import email
84import email.Iterators
85import email.Header
86import re
87import urllib
88import unicodedata
89from stat import *
90import mimetypes
91import traceback
92
93
94# Will fail where unavailable, e.g. Windows
95#
96try:
97    import syslog
98    SYSLOG_AVAILABLE = True
99except ImportError:
100    SYSLOG_AVAILABLE = False
101
102from datetime import tzinfo, timedelta, datetime
103from trac import config as trac_config
104
105# Some global variables
106#
107trac_default_version = '0.10'
108m = None
109
110# A UTC class needed for trac version 0.11, added by
111# tbaschak at ktc dot mb dot ca
112#
113class UTC(tzinfo):
114        """UTC"""
115        ZERO = timedelta(0)
116        HOUR = timedelta(hours=1)
117       
118        def utcoffset(self, dt):
119                return self.ZERO
120               
121        def tzname(self, dt):
122                return "UTC"
123               
124        def dst(self, dt):
125                return self.ZERO
126
127
128class TicketEmailParser(object):
129        env = None
130        comment = '> '
131   
132        def __init__(self, env, parameters, version):
133                self.env = env
134
135                # Database connection
136                #
137                self.db = None
138
139                # Save parameters
140                #
141                self.parameters = parameters
142
143                # Some useful mail constants
144                #
145                self.author = None
146                self.email_addr = None
147                self.email_from = None
148
149                self.VERSION = version
150                self.DRY_RUN = parameters['dry_run']
151
152                self.get_config = self.env.config.get
153
154                if parameters.has_key('umask'):
155                        os.umask(int(parameters['umask'], 8))
156
157                if parameters.has_key('debug'):
158                        self.DEBUG = int(parameters['debug'])
159                else:
160                        self.DEBUG = 0
161
162                if parameters.has_key('mailto_link'):
163                        self.MAILTO = int(parameters['mailto_link'])
164                        if parameters.has_key('mailto_cc'):
165                                self.MAILTO_CC = parameters['mailto_cc']
166                        else:
167                                self.MAILTO_CC = ''
168                else:
169                        self.MAILTO = 0
170
171                if parameters.has_key('spam_level'):
172                        self.SPAM_LEVEL = int(parameters['spam_level'])
173                else:
174                        self.SPAM_LEVEL = 0
175
176                if parameters.has_key('spam_header'):
177                        self.SPAM_HEADER = parameters['spam_header']
178                else:
179                        self.SPAM_HEADER = 'X-Spam-Score'
180
181                if parameters.has_key('email_quote'):
182                        self.EMAIL_QUOTE = str(parameters['email_quote'])
183                else:   
184                        self.EMAIL_QUOTE = '> '
185
186                if parameters.has_key('email_header'):
187                        self.EMAIL_HEADER = int(parameters['email_header'])
188                else:
189                        self.EMAIL_HEADER = 0
190
191                if parameters.has_key('alternate_notify_template'):
192                        self.notify_template = str(parameters['alternate_notify_template'])
193                else:
194                        self.notify_template = None
195
196                if parameters.has_key('reply_all'):
197                        self.REPLY_ALL = int(parameters['reply_all'])
198                else:
199                        self.REPLY_ALL = 0
200
201                if parameters.has_key('ticket_update'):
202                        self.TICKET_UPDATE = int(parameters['ticket_update'])
203                else:
204                        self.TICKET_UPDATE = 0
205
206                if parameters.has_key('drop_spam'):
207                        self.DROP_SPAM = int(parameters['drop_spam'])
208                else:
209                        self.DROP_SPAM = 0
210
211                if parameters.has_key('verbatim_format'):
212                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
213                else:
214                        self.VERBATIM_FORMAT = 1
215
216                if parameters.has_key('strip_signature'):
217                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
218                else:
219                        self.STRIP_SIGNATURE = 0
220
221                if parameters.has_key('strip_quotes'):
222                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
223                else:
224                        self.STRIP_QUOTES = 0
225
226                if parameters.has_key('use_textwrap'):
227                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
228                else:
229                        self.USE_TEXTWRAP = 0
230
231                if parameters.has_key('python_egg_cache'):
232                        self.python_egg_cache = str(parameters['python_egg_cache'])
233                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
234
235                # Use OS independend functions
236                #
237                self.TMPDIR = os.path.normcase('/tmp')
238                if parameters.has_key('tmpdir'):
239                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
240
241                if parameters.has_key('ignore_trac_user_settings'):
242                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
243                else:
244                        self.IGNORE_TRAC_USER_SETTINGS = 0
245
246        def spam(self, message):
247                """
248                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
249                # Note if Spam_level then '*' are included
250                """
251                spam = False
252                if message.has_key(self.SPAM_HEADER):
253                        spam_l = string.split(message[self.SPAM_HEADER])
254
255                        try:
256                                number = spam_l[0].count('*')
257                        except IndexError, detail:
258                                number = 0
259                               
260                        if number >= self.SPAM_LEVEL:
261                                spam = True
262                               
263                # treat virus mails as spam
264                #
265                elif message.has_key('X-Virus-found'):                 
266                        spam = True
267
268                # How to handle SPAM messages
269                #
270                if self.DROP_SPAM and spam:
271                        if self.DEBUG > 2 :
272                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
273
274                        return 'drop'   
275
276                elif spam:
277
278                        return 'Spam'   
279
280                else:
281
282                        return False
283
284
285        def blacklisted_from(self):
286                """
287                This function wil check if the email address is in the black list
288                """
289                try:
290                        black_list = self.parameters['black_list']
291                except KeyError, detail:
292                        black_list = 'MAILER-DAEMON@'
293
294                black_list = string.split(black_list, ',')     
295                if not black_list.count('MAILER-DAEMON@'):
296                        black_list.append('MAILER-DAEMON@')
297
298                for entry in black_list:       
299                        entry = entry.strip()
300                        FROM_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
301                        result =  FROM_RE.search(self.email_addr)
302                        if result:
303                                return True
304
305                return False
306
307        def email_to_unicode(self, message_str):
308                """
309                Email has 7 bit ASCII code, convert it to unicode with the charset
310        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
311                understands it.
312                """
313                results =  email.Header.decode_header(message_str)
314                str = None
315                for text,format in results:
316                        if format:
317                                try:
318                                        temp = unicode(text, format)
319                                except UnicodeError, detail:
320                                        # This always works
321                                        #
322                                        temp = unicode(text, 'iso-8859-15')
323                                except LookupError, detail:
324                                        #text = 'ERROR: Could not find charset: %s, please install' %format
325                                        #temp = unicode(text, 'iso-8859-15')
326                                        temp = message_str
327                                       
328                        else:
329                                temp = string.strip(text)
330                                temp = unicode(text, 'iso-8859-15')
331
332                        if str:
333                                str = '%s %s' %(str, temp)
334                        else:
335                                str = '%s' %temp
336
337                #str = str.encode('utf-8')
338                return str
339
340        def debug_attachments(self, message):
341                n = 0
342                for part in message.walk():
343                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
344                                print 'TD: multipart container'
345                                continue
346
347                        n = n + 1
348                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
349                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
350
351                        if part.is_multipart():
352                                print 'TD: this part is multipart'
353                                payload = part.get_payload(decode=1)
354                                print 'TD: payload:', payload
355                        else:
356                                print 'TD: this part is not multipart'
357
358                        file = 'part%d' %n
359                        part_file = os.path.join(self.TMPDIR, file)
360                        #part_file = '/var/tmp/part%d' % n
361                        print 'TD: writing part%d (%s)' % (n,part_file)
362                        fx = open(part_file, 'wb')
363                        text = part.get_payload(decode=1)
364                        if not text:
365                                text = '(None)'
366                        fx.write(text)
367                        fx.close()
368                        try:
369                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
370                        except OSError:
371                                pass
372
373        def email_header_txt(self, m):
374                """
375                Display To and CC addresses in description field
376                """
377                str = ''
378                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
379                if m['To'] and len(m['To']) > 0:
380                        str = "'''To:''' %s\r\n" %(m['To'])
381                if m['Cc'] and len(m['Cc']) > 0:
382                        str = "%s'''Cc:''' %s\r\n" % (str, m['Cc'])
383
384                return  self.email_to_unicode(str)
385
386
387        def set_owner(self, ticket):
388                """
389                Select default owner for ticket component
390                """
391                #### return self.get_config('ticket', 'default_component')
392                cursor = self.db.cursor()
393                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
394                cursor.execute(sql)
395                try:
396                        ticket['owner'] = cursor.fetchone()[0]
397                except TypeError, detail:
398                        ticket['owner'] = None
399
400        def get_sender_info(self, message):
401                """
402                Get the default author name and email address from the message
403                """
404
405                self.email_from = self.email_to_unicode(message['from'])
406                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
407
408                # Maybe for later user
409                #self.email_from =  self.email_to_unicode(self.email_addr)
410
411
412                if self.IGNORE_TRAC_USER_SETTINGS:
413                        return
414
415                # Is this a registered user, use email address as search key:
416                # result:
417                #   u : login name
418                #   n : Name that the user has set in the settings tab
419                #   e : email address that the user has set in the settings tab
420                #
421                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
422                                if e == self.email_addr ]
423
424                if len(users) == 1:
425                        self.email_from = users[0][0]
426
427        def set_reply_fields(self, ticket, message):
428                """
429                Set all the right fields for a new ticket
430                """
431                ticket['reporter'] = self.email_from
432
433                # Put all CC-addresses in ticket CC field
434                #
435                if self.REPLY_ALL:
436                        #tos = message.get_all('to', [])
437                        ccs = message.get_all('cc', [])
438
439                        addrs = email.Utils.getaddresses(ccs)
440                        if not addrs:
441                                return
442
443                        # Remove reporter email address if notification is
444                        # on
445                        #
446                        if self.notification:
447                                try:
448                                        addrs.remove((self.author, self.email_addr))
449                                except ValueError, detail:
450                                        pass
451
452                        for name,mail in addrs:
453                                try:
454                                        mail_list = '%s, %s' %(mail_list, mail)
455                                except:
456                                        mail_list = mail
457
458                        if mail_list:
459                                ticket['cc'] = self.email_to_unicode(mail_list)
460
461        def save_email_for_debug(self, message, tempfile=False):
462                if tempfile:
463                        import tempfile
464                        msg_file = tempfile.mktemp('.email2trac')
465                else:
466                        #msg_file = '/var/tmp/msg.txt'
467                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
468
469                print 'TD: saving email to %s' % msg_file
470                fx = open(msg_file, 'wb')
471                fx.write('%s' % message)
472                fx.close()
473                try:
474                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
475                except OSError:
476                        pass
477
478        def str_to_dict(self, str):
479                """
480                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
481                """
482                # Skip the last ':' character
483                #
484                fields = string.split(str[:-1], ',')
485
486                result = dict()
487                for field in fields:
488                        try:
489                                index, value = string.split(field,'=')
490
491                                # We can not change the description of a ticket via the subject
492                                # line. The description is the body of the email
493                                #
494                                if index.lower() in ['description']:
495                                        continue
496
497                                if value:
498                                        result[index.lower()] = value
499
500                        except ValueError:
501                                pass
502
503                return result
504
505        def update_ticket_fields(self, ticket, user_dict, use_default=None):
506                """
507                This will update the ticket fields. It will check if the
508                given fields are known and if the right values are specified
509                It will only update the ticket field value:
510                        - If the field is known
511                        - If the value supplied is valid for the ticket field.
512                          If not then there are two options:
513                           1) Skip the value (use_default=None)
514                           2) Set default value for field (use_default=1)
515                """
516
517                # Build a system dictionary from the ticket fields
518                # with field as index and option as value
519                #
520                sys_dict = dict()
521                for field in ticket.fields:
522                        try:
523                                sys_dict[field['name']] = field['options']
524
525                        except KeyError:
526                                sys_dict[field['name']] = None
527                                pass
528
529                # Check user supplied fields an compare them with the
530                # system one's
531                #
532                for field,value in user_dict.items():
533                        if self.DEBUG >= 10:
534                                print  'user_field\t %s = %s' %(field,value)
535
536                        if sys_dict.has_key(field):
537
538                                # Check if value is an allowed system option, if TypeError then
539                                # every value is allowed
540                                #
541                                try:
542                                        if value in sys_dict[field]:
543                                                ticket[field] = value
544                                        else:
545                                                # Must we set a default if value is not allowed
546                                                #
547                                                if use_default:
548                                                        value = self.get_config('ticket', 'default_%s' %(field) )
549                                                        ticket[field] = value
550
551                                except TypeError:
552                                        ticket[field] = value
553
554                                if self.DEBUG >= 10:
555                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
556                                       
557        def ticket_update(self, m, spam):
558                """
559                If the current email is a reply to an existing ticket, this function
560                will append the contents of this email to that ticket, instead of
561                creating a new one.
562                """
563
564                if not m['Subject']:
565                        return False
566                else:
567                        subject  = self.email_to_unicode(m['Subject'])
568
569                # [hic] #1529: Re: LRZ
570                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
571                #
572                TICKET_RE = re.compile(r"""
573                                        (?P<ticketnr>[#][0-9]+:)
574                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
575                                        """, re.VERBOSE)
576
577                result =  TICKET_RE.search(subject)
578                if not result:
579                        return False
580
581                # Must we update ticket fields
582                #
583                update_fields = dict()
584                try:
585                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
586                        update_fields = self.str_to_dict(keywords)
587
588                        # Strip '#'
589                        #
590                        ticket_id = int(nr[1:])
591
592                except AttributeError:
593                        # Strip '#' and ':'
594                        #
595                        nr = result.group('ticketnr')
596                        ticket_id = int(nr[1:-1])
597
598
599                # When is the change committed
600                #
601                #
602                if self.VERSION == 0.11:
603                        utc = UTC()
604                        when = datetime.now(utc)
605                else:
606                        when = int(time.time())
607
608                try:
609                        tkt = Ticket(self.env, ticket_id, self.db)
610                except util.TracError, detail:
611                        return False
612
613                # reopen the ticket if it is was closed
614                # We must use the ticket workflow framework
615                #
616                if tkt['status'] in ['closed']:
617                        tkt['status'] = 'reopened'
618                        tkt['resolution'] = ''
619
620                # Must we update some ticket fields properties
621                #
622                if update_fields:
623                        self.update_ticket_fields(tkt, update_fields)
624
625                body_text = self.get_body_text(m)
626
627                if self.EMAIL_HEADER:
628                        head = self.email_header_txt(m)
629                        body_text = u"%s\r\n%s" %(head, body_text)
630
631                if body_text.strip():
632                        tkt.save_changes(self.author, body_text, when)
633
634                tkt['id'] = ticket_id
635
636                if self.VERSION  == 0.9:
637                        str = self.attachments(m, tkt, True)
638                else:
639                        str = self.attachments(m, tkt)
640
641                if self.notification and not spam:
642                        self.notify(tkt, False, when)
643
644                return True
645
646        def set_ticket_fields(self, ticket):
647                """
648                set the ticket fields to value specified
649                        - /etc/email2trac.conf with <prefix>_<field>
650                        - trac default values, trac.ini
651                """
652                user_dict = dict()
653
654                for field in ticket.fields:
655
656                        name = field['name']
657
658                        # skip some fields like resolution
659                        #
660                        if name in [ 'resolution' ]:
661                                continue
662
663                        # default trac value
664                        #
665                        value = self.get_config('ticket', 'default_%s' %(name) )
666                        if self.DEBUG > 10:
667                                print 'trac.ini name %s = %s' %(name, value)
668
669                        prefix = self.parameters['ticket_prefix']
670                        try:
671                                value = self.parameters['%s_%s' %(prefix, name)]
672                                if self.DEBUG > 10:
673                                        print 'email2trac.conf %s = %s ' %(name, value)
674
675                        except KeyError, detail:
676                                pass
677               
678                        if self.DEBUG:
679                                print 'user_dict[%s] = %s' %(name, value)
680
681                        user_dict[name] = value
682
683                self.update_ticket_fields(ticket, user_dict, use_default=1)
684
685                # Set status ticket
686                #`
687                ticket['status'] = 'new'
688
689
690
691        def new_ticket(self, msg, spam):
692                """
693                Create a new ticket
694                """
695                tkt = Ticket(self.env)
696
697                self.set_ticket_fields(tkt)
698
699                # Some defaults
700                #
701                #tkt['status'] = 'new'
702                #tkt['milestone'] = self.get_config('ticket', 'default_milestone')
703                #tkt['priority'] = self.get_config('ticket', 'default_priority')
704                #tkt['severity'] = self.get_config('ticket', 'default_severity')
705                #tkt['version'] = self.get_config('ticket', 'default_version')
706                #tkt['type'] = self.get_config('ticket', 'default_type')
707
708                # Old style setting for component, will be removed
709                #
710                if spam:
711                        tkt['component'] = 'Spam'
712
713                elif self.parameters.has_key('component'):
714                        tkt['component'] = self.parameters['component']
715
716                if not msg['Subject']:
717                        tkt['summary'] = u'(No subject)'
718                else:
719                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
720
721
722                self.set_reply_fields(tkt, msg)
723
724                # produce e-mail like header
725                #
726                head = ''
727                if self.EMAIL_HEADER > 0:
728                        head = self.email_header_txt(msg)
729                       
730                body_text = self.get_body_text(msg)
731
732                tkt['description'] = '%s\r\n%s' \
733                        %(head, body_text)
734
735                #when = int(time.time())
736                #
737                utc = UTC()
738                when = datetime.now(utc)
739
740                if self.DRY_RUN:
741                        ticket_id = 'DRY_RUN'
742                else:
743                        ticket_id = tkt.insert()
744                       
745                tkt['id'] = ticket_id
746
747                changed = False
748                comment = ''
749
750                # Rewrite the description if we have mailto enabled
751                #
752                if self.MAILTO:
753                        changed = True
754                        comment = u'\nadded mailto line\n'
755                        #mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
756                        mailto = self.html_mailto_link( m['Subject'], ticket_id, body_text)
757                        tkt['description'] = u'%s\r\n%s%s\r\n' \
758                                %(head, mailto, body_text)
759
760                str =  self.attachments(msg, tkt)
761                if str:
762                        changed = True
763                        comment = '%s\n%s\n' %(comment, str)
764
765                if changed:
766                        if self.DRY_RUN:
767                                print 'DRY_RUN: tkt.save_changes(self.author, comment)'
768                        else:
769                                tkt.save_changes(self.author, comment)
770                                #print tkt.get_changelog(self.db, when)
771
772                if self.notification:
773                        if self.DRY_RUN:
774                                print 'DRY_RUN: self.notify(tkt, True)'
775                        else:
776                                if not spam:
777                                        self.notify(tkt, True)
778                                #self.notify(tkt, False)
779
780        def parse(self, fp):
781                global m
782
783                m = email.message_from_file(fp)
784                if not m:
785                        return
786
787                if self.DEBUG > 1:        # save the entire e-mail message text
788                        self.save_email_for_debug(m, True)
789                        self.debug_attachments(m)
790
791                self.db = self.env.get_db_cnx()
792                self.get_sender_info(m)
793
794                if self.blacklisted_from():
795                        if self.DEBUG > 1 :
796                                print 'Message rejected : From: in blacklist'
797                        return False
798
799                # If drop the message
800                #
801                if self.spam(m) == 'drop':
802                        return False
803
804                elif self.spam(m) == 'spam':
805                        spam_msg = True
806
807                else:
808                        spam_msg = False
809
810                if self.get_config('notification', 'smtp_enabled') in ['true']:
811                        self.notification = 1
812                else:
813                        self.notification = 0
814
815                # Must we update existing tickets
816                #
817                if self.TICKET_UPDATE > 0:
818                        if self.ticket_update(m, spam_msg):
819                                return True
820
821                self.new_ticket(m, spam_msg)
822
823        def strip_signature(self, text):
824                """
825                Strip signature from message, inspired by Mailman software
826                """
827                body = []
828                for line in text.splitlines():
829                        if line == '-- ':
830                                break
831                        body.append(line)
832
833                return ('\n'.join(body))
834
835        def strip_quotes(self, text):
836                """
837                Strip quotes from message by Nicolas Mendoza
838                """
839                body = []
840                for line in text.splitlines():
841                        if line.startswith(self.EMAIL_QUOTE):
842                                continue
843                        body.append(line)
844
845                return ('\n'.join(body))
846
847        def wrap_text(self, text, replace_whitespace = False):
848                """
849                Will break a lines longer then given length into several small
850                lines of size given length
851                """
852                import textwrap
853
854                LINESEPARATOR = '\n'
855                reformat = ''
856
857                for s in text.split(LINESEPARATOR):
858                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
859                        if tmp:
860                                reformat = '%s\n%s' %(reformat,tmp)
861                        else:
862                                reformat = '%s\n' %reformat
863
864                return reformat
865
866                # Python2.4 and higher
867                #
868                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
869                #
870
871
872        def get_body_text(self, msg):
873                """
874                put the message text in the ticket description or in the changes field.
875                message text can be plain text or html or something else
876                """
877                has_description = 0
878                encoding = True
879                ubody_text = u'No plain text message'
880                for part in msg.walk():
881
882                        # 'multipart/*' is a container for multipart messages
883                        #
884                        if part.get_content_maintype() == 'multipart':
885                                continue
886
887                        if part.get_content_type() == 'text/plain':
888                                # Try to decode, if fails then do not decode
889                                #
890                                body_text = part.get_payload(decode=1)
891                                if not body_text:                       
892                                        body_text = part.get_payload(decode=0)
893       
894                                if self.STRIP_SIGNATURE:
895                                        body_text = self.strip_signature(body_text)
896
897                                if self.STRIP_QUOTES:
898                                        body_text = self.strip_quotes(body_text)
899
900                                if self.USE_TEXTWRAP:
901                                        body_text = self.wrap_text(body_text)
902
903                                # Get contents charset (iso-8859-15 if not defined in mail headers)
904                                #
905                                charset = part.get_content_charset()
906                                if not charset:
907                                        charset = 'iso-8859-15'
908
909                                try:
910                                        ubody_text = unicode(body_text, charset)
911
912                                except UnicodeError, detail:
913                                        ubody_text = unicode(body_text, 'iso-8859-15')
914
915                                except LookupError, detail:
916                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
917
918                        elif part.get_content_type() == 'text/html':
919                                ubody_text = '(see attachment for HTML mail message)'
920
921                        else:
922                                ubody_text = '(see attachment for message)'
923
924                        has_description = 1
925                        break           # we have the description, so break
926
927                if not has_description:
928                        ubody_text = '(see attachment for message)'
929
930                # A patch so that the web-interface will not update the description
931                # field of a ticket
932                #
933                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
934
935                #  If we can unicode it try to encode it for trac
936                #  else we a lot of garbage
937                #
938                #if encoding:
939                #       ubody_text = ubody_text.encode('utf-8')
940
941                if self.VERBATIM_FORMAT:
942                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
943                else:
944                        ubody_text = '%s' %ubody_text
945
946                return ubody_text
947
948        def notify(self, tkt , new=True, modtime=0):
949                """
950                A wrapper for the TRAC notify function. So we can use templates
951                """
952                try:
953                        # create false {abs_}href properties, to trick Notify()
954                        #
955                        if not self.VERSION == 0.11:
956                                self.env.abs_href = Href(self.get_config('project', 'url'))
957                                self.env.href = Href(self.get_config('project', 'url'))
958
959                        tn = TicketNotifyEmail(self.env)
960
961                        if self.notify_template:
962                                tn.template_name = self.notify_template;
963
964                        tn.notify(tkt, new, modtime)
965
966                except Exception, e:
967                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
968
969        def html_mailto_link(self, subject, id, body):
970                if not self.author:
971                        author = self.email_addr
972                else:   
973                        author = self.author
974
975                # Must find a fix
976                #
977                #arr = string.split(body, '\n')
978                #arr = map(self.mail_line, arr)
979                #body = string.join(arr, '\n')
980                #body = '%s wrote:\n%s' %(author, body)
981
982                # Temporary fix
983                #
984                str = 'mailto:%s?Subject=%s&Cc=%s' %(
985                       urllib.quote(self.email_addr),
986                           urllib.quote('Re: #%s: %s' %(id, subject)),
987                           urllib.quote(self.MAILTO_CC)
988                           )
989
990                str = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(str, author)
991                return str
992
993        def attachments(self, message, ticket, update=False):
994                '''
995                save any attachments as files in the ticket's directory
996                '''
997                count = 0
998                first = 0
999                number = 0
1000
1001                # Get Maxium attachment size
1002                #
1003                max_size = int(self.get_config('attachment', 'max_size'))
1004                status   = ''
1005
1006                for part in message.walk():
1007                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
1008                                continue
1009
1010                        if not first:                                                                           # first content is the message
1011                                first = 1
1012                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
1013                                        continue
1014
1015                        filename = part.get_filename()
1016                        if not filename:
1017                                number = number + 1
1018                                filename = 'part%04d' % number
1019
1020                                ext = mimetypes.guess_extension(part.get_content_type())
1021                                if not ext:
1022                                        ext = '.bin'
1023
1024                                filename = '%s%s' % (filename, ext)
1025                        else:
1026                                filename = self.email_to_unicode(filename)
1027
1028                        # From the trac code
1029                        #
1030                        filename = filename.replace('\\', '/').replace(':', '/')
1031                        filename = os.path.basename(filename)
1032
1033                        # We try to normalize the filename to utf-8 NFC if we can.
1034                        # Files uploaded from OS X might be in NFD.
1035                        # Check python version and then try it
1036                        #
1037                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1038                                try:
1039                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1040                                except TypeError:
1041                                        pass
1042
1043                        url_filename = urllib.quote(filename)
1044                        #
1045                        # Must be tuneables HvB
1046                        #
1047                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
1048                        text = part.get_payload(decode=1)
1049                        if not text:
1050                                text = '(None)'
1051                        fd.write(text)
1052                        fd.close()
1053
1054                        # get the file_size
1055                        #
1056                        stats = os.lstat(path)
1057                        file_size = stats[stat.ST_SIZE]
1058
1059                        # Check if the attachment size is allowed
1060                        #
1061                        if (max_size != -1) and (file_size > max_size):
1062                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1063                                        %(status, filename, file_size, max_size)
1064
1065                                os.unlink(path)
1066                                continue
1067                        else:
1068                                count = count + 1
1069                                       
1070                        # Insert the attachment
1071                        #
1072                        fd = open(path)
1073                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
1074
1075                        # This will break the ticket_update system, the body_text is vaporized
1076                        # ;-(
1077                        #
1078                        if not update:
1079                                att.author = self.author
1080                                att.description = self.email_to_unicode('Added by email2trac')
1081
1082                        att.insert(url_filename, fd, file_size)
1083                        #except  util.TracError, detail:
1084                        #       print detail
1085
1086                        # Remove the created temporary filename
1087                        #
1088                        fd.close()
1089                        os.unlink(path)
1090
1091                # Return how many attachments
1092                #
1093                status = 'This message has %d attachment(s)\n%s' %(count, status)
1094                return status
1095
1096
1097def mkdir_p(dir, mode):
1098        '''do a mkdir -p'''
1099
1100        arr = string.split(dir, '/')
1101        path = ''
1102        for part in arr:
1103                path = '%s/%s' % (path, part)
1104                try:
1105                        stats = os.stat(path)
1106                except OSError:
1107                        os.mkdir(path, mode)
1108
1109def ReadConfig(file, name):
1110        """
1111        Parse the config file
1112        """
1113        if not os.path.isfile(file):
1114                print 'File %s does not exist' %file
1115                sys.exit(1)
1116
1117        config = trac_config.Configuration(file)
1118
1119        # Use given project name else use defaults
1120        #
1121        if name:
1122                sections = config.sections()
1123                if not name in sections:
1124                        print "Not a valid project name: %s" %name
1125                        print "Valid names: %s" %sections
1126                        sys.exit(1)
1127
1128                project =  dict()
1129                for option, value in  config.options(name):
1130                        project[option] = value
1131
1132        else:
1133                # use some trac internales to get the defaults
1134                #
1135                project = config.parser.defaults()
1136
1137        return project
1138
1139
1140if __name__ == '__main__':
1141        # Default config file
1142        #
1143        configfile = '@email2trac_conf@'
1144        project = ''
1145        component = ''
1146        ticket_prefix = 'default'
1147        dry_run = None
1148
1149        ENABLE_SYSLOG = 0
1150
1151
1152        SHORT_OPT = 'chf:np:t:'
1153        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1154
1155        try:
1156                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1157        except getopt.error,detail:
1158                print __doc__
1159                print detail
1160                sys.exit(1)
1161       
1162        project_name = None
1163        for opt,value in opts:
1164                if opt in [ '-h', '--help']:
1165                        print __doc__
1166                        sys.exit(0)
1167                elif opt in ['-c', '--component']:
1168                        component = value
1169                elif opt in ['-f', '--file']:
1170                        configfile = value
1171                elif opt in ['-n', '--dry-run']:
1172                        dry_run = True
1173                elif opt in ['-p', '--project']:
1174                        project_name = value
1175                elif opt in ['-t', '--ticket_prefix']:
1176                        ticket_prefix = value
1177       
1178        settings = ReadConfig(configfile, project_name)
1179        if not settings.has_key('project'):
1180                print __doc__
1181                print 'No Trac project is defined in the email2trac config file.'
1182                sys.exit(1)
1183       
1184        if component:
1185                settings['component'] = component
1186
1187        # The default prefix for ticket values in email2trac.conf
1188        #
1189        settings['ticket_prefix'] = ticket_prefix
1190        settings['dry_run'] = dry_run
1191       
1192        if settings.has_key('trac_version'):
1193                version = settings['trac_version']
1194        else:
1195                version = trac_default_version
1196
1197
1198        #debug HvB
1199        #print settings
1200
1201        try:
1202                if version == '0.9':
1203                        from trac import attachment
1204                        from trac.env import Environment
1205                        from trac.ticket import Ticket
1206                        from trac.web.href import Href
1207                        from trac import util
1208                        from trac.Notify import TicketNotifyEmail
1209                elif version == '0.10':
1210                        from trac import attachment
1211                        from trac.env import Environment
1212                        from trac.ticket import Ticket
1213                        from trac.web.href import Href
1214                        from trac import util
1215                        #
1216                        # return  util.text.to_unicode(str)
1217                        #
1218                        # see http://projects.edgewall.com/trac/changeset/2799
1219                        from trac.ticket.notification import TicketNotifyEmail
1220                        from trac import config as trac_config
1221                elif version == '0.11':
1222                        from trac import attachment
1223                        from trac.env import Environment
1224                        from trac.ticket import Ticket
1225                        from trac.web.href import Href
1226                        from trac import config as trac_config
1227                        from trac import util
1228                        #
1229                        # return  util.text.to_unicode(str)
1230                        #
1231                        # see http://projects.edgewall.com/trac/changeset/2799
1232                        from trac.ticket.notification import TicketNotifyEmail
1233                else:
1234                        print 'TRAC version %s is not supported' %version
1235                        sys.exit(1)
1236                       
1237                if settings.has_key('enable_syslog'):
1238                        if SYSLOG_AVAILABLE:
1239                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1240
1241                env = Environment(settings['project'], create=0)
1242                tktparser = TicketEmailParser(env, settings, float(version))
1243                tktparser.parse(sys.stdin)
1244
1245        # Catch all errors ans log to SYSLOG if we have enabled this
1246        # else stdout
1247        #
1248        except Exception, error:
1249                if ENABLE_SYSLOG:
1250                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1251
1252                        etype, evalue, etb = sys.exc_info()
1253                        for e in traceback.format_exception(etype, evalue, etb):
1254                                syslog.syslog(e)
1255
1256                        syslog.closelog()
1257                else:
1258                        traceback.print_exc()
1259
1260                if m:
1261                        tktparser.save_email_for_debug(m, True)
1262
1263# EOB
Note: See TracBrowser for help on using the repository browser.