source: trunk/email2trac.py.in @ 341

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

moved email_to_unicode to conversion section

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