source: trunk/email2trac.py.in @ 222

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

email2trac.py.in, email2trac.conf:

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