~m-buck/+junk/gtk-desktop-info

« back to all changes in this revision

Viewing changes to plugin_email.py

  • Committer: Mark Buck (Kaivalagi)
  • Date: 2009-06-19 17:13:00 UTC
  • Revision ID: m_buck@hotmail.com-20090619171300-5cbhr90xwg62z27y
Added --backgroundblend and --backgroundcolour options for visual seperation of output from wallpaper if required, Fixed song length output in the rhythmbox plugin when songs are an hour long or more, Added copy option to right click, enabling the copying of html content to the clipboard for testing, Moved common functions into a plugin_common module

Show diffs side-by-side

added added

removed removed

Lines of Context:
8
8
# Created: 23/11/2008
9
9
from datetime import datetime
10
10
from email.header import decode_header
11
 
from htmlentitydefs import name2codepoint, codepoint2name
12
11
from optparse import OptionParser
 
12
from plugin_common import getHTMLText, getTypedValue
 
13
import codecs
 
14
import fileinput
 
15
import imaplib
 
16
import logging
 
17
import os
13
18
import poplib
14
 
import imaplib
15
 
import socket
16
19
import re
17
 
import os
18
 
import codecs
19
20
import shutil
 
21
import socket
20
22
import traceback
21
 
import logging
22
 
import fileinput
 
23
 
23
24
 
24
25
app_name = "gtk-desktop-info"
25
26
app_path = os.path.dirname(os.path.abspath(__file__))
38
39
 
39
40
    def __cmp__(self, other):
40
41
        return cmp(self.getRecvDate(self.recvdate), self.getRecvDate(other.recvdate))
41
 
    
 
42
 
42
43
    def getRecvDate(self, recvdate):
43
44
        if recvdate is None:
44
45
            return datetime.now()
48
49
class EmailConfig:
49
50
    HEADERTEMPLATE = None
50
51
    TEMPLATE = None
51
 
    CONNECTIONTIMEOUT = 10           
 
52
    CONNECTIONTIMEOUT = 10
52
53
    MAILINFO = 0
53
54
    FOLDER = "Inbox"
54
 
    
 
55
 
55
56
class Output:
56
 
    
 
57
 
57
58
    IMAP_SEARCH_OPTION = "UNSEEN" # "RECENT"
58
59
    POP_FETCH_OPTION = "TOP" # "RETR"
59
 
    
 
60
 
60
61
    emaillist = []
61
62
    logger = None
62
63
    options = None
67
68
        self.loadConfigData()
68
69
 
69
70
    def loadConfigData(self):
70
 
        try:         
 
71
        try:
71
72
            self.config = EmailConfig()
72
 
            
 
73
 
73
74
            if self.options.config != None:
74
75
                # load the config based on options passed in from the main app
75
76
                configfilepath = self.options.config
76
77
            else:
77
78
                # load plugin config from home directory of the user
78
79
                configfilepath = os.path.join(os.path.expanduser('~'), ".config/"+app_name+"/"+module_name+".config")
79
 
                            
 
80
 
80
81
            if os.path.exists(configfilepath):
81
 
                
 
82
 
82
83
                self.logger.info("Loading config settings from \"%s\""%configfilepath)
83
84
 
84
85
                for line in fileinput.input(os.path.expanduser(configfilepath)):
85
86
                    line = line.strip()
86
87
                    if len(line) > 0 and line[0:1] != "#": # ignore commented lines or empty ones
87
 
    
 
88
 
88
89
                        name = line.split("=")[0].strip().upper() # config setting name on the left of =
89
90
                        value = line.split("=")[1].split("#")[0].strip() # config value on the right of = (minus any trailing comments)
90
 
                                                     
 
91
 
91
92
                        if len(value) > 0:
92
93
                            if name == "HEADERTEMPLATE":
93
 
                                self.config.HEADERTEMPLATE = self.getTypedValue(value, "string")                            
 
94
                                self.config.HEADERTEMPLATE = getTypedValue(value, "string")
94
95
                            elif name == "TEMPLATE":
95
 
                                self.config.TEMPLATE = self.getTypedValue(value, "string")
 
96
                                self.config.TEMPLATE = getTypedValue(value, "string")
96
97
                            elif name == "CONNECTIONTIMEOUT":
97
 
                                self.config.CONNECTIONTIMEOUT = self.getTypedValue(value, "integer")
 
98
                                self.config.CONNECTIONTIMEOUT = getTypedValue(value, "integer")
98
99
                            elif name == "MAILINFO":
99
 
                                self.config.MAILINFO = self.getTypedValue(value, "integer")
 
100
                                self.config.MAILINFO = getTypedValue(value, "integer")
100
101
                            elif name == "FOLDER":
101
 
                                self.config.FOLDER = self.getTypedValue(value, "string")                                                                                                                     
 
102
                                self.config.FOLDER = getTypedValue(value, "string")
102
103
                            else:
103
104
                                self.logger.error("Unknown option in config file: " + name)
104
105
            else:
105
106
                self.logger.info("Config data file %s not found, using defaults and setting up config file for next time" % configfilepath)
106
 
                
 
107
 
107
108
                userconfigpath = os.path.join(os.path.expanduser('~'), ".config/"+app_name+"/")
108
109
                configsource = os.path.join(app_path, "config/"+module_name+".config")
109
 
                
 
110
 
110
111
                if os.path.exists(userconfigpath) == False:
111
112
                    os.makedirs(userconfigpath)
112
113
 
115
116
        except Exception, e:
116
117
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())
117
118
 
118
 
    def getTypedValue(self, value, expectedtype):
119
 
        
120
 
        try:
121
 
            if len(value.strip(" ")) == 0:
122
 
                return None
123
 
            
124
 
            elif value.lower() == "true":
125
 
                if expectedtype == "boolean":
126
 
                    return True
127
 
                else:
128
 
                    self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
129
 
                    
130
 
            elif value.lower() == "false":
131
 
                if expectedtype == "boolean":
132
 
                    return False
133
 
                else:
134
 
                    self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
135
 
                    
136
 
            elif self.isNumeric(value) == True:
137
 
                if expectedtype == "integer":
138
 
                    return int(value)
139
 
                else:
140
 
                    self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
141
 
                    
142
 
            else:
143
 
                return value
144
 
 
145
 
        except (TypeError, ValueError):
146
 
            self.logger.error("Cannot convert '%s' to expected type of '%s'"%(value,expectedtype))
147
 
            return value        
148
 
        
149
119
    def getTemplateList(self,template):
150
120
 
151
121
        templatelist = []
152
 
    
 
122
 
153
123
        for template_part in template.split("{"):
154
124
            if template_part != "":
155
125
                for template_part in template_part.split("}"):
161
131
    def getOutputData(self,servertype,servername,port,folder,ssl,username,password,connectiontimeout,mailinfo):
162
132
        try:
163
133
            output = u""
164
 
            
 
134
 
165
135
            socket.setdefaulttimeout(connectiontimeout)
166
 
        
 
136
 
167
137
            if servertype == "POP":
168
138
                count = self.getPOPEmailData(servername,port,folder,ssl,username,password,mailinfo)
169
139
            elif servertype == "IMAP":
177
147
            elif count == 0:
178
148
                output = "0"
179
149
            else:
180
 
                
 
150
 
181
151
                if mailinfo > 0:
182
152
 
183
153
                    output = "%s New"%count
184
 
                    
 
154
 
185
155
                    counter = 0
186
156
                    self.emaillist.sort(reverse=True)
187
157
                    for emaildata in self.emaillist:
188
158
                        counter = counter + 1
189
159
                        if mailinfo >= counter:
190
 
                            output = output + "\n<br>%s. %s: \"%s\""%(counter,emaildata.sender,emaildata.subject)                            
 
160
                            output = output + "\n<br>%s. %s: \"%s\""%(counter,emaildata.sender,emaildata.subject)
191
161
                else:
192
162
                    output = str(count)
193
 
                    
 
163
 
194
164
            return output
195
165
 
196
166
        except Exception, e:
197
 
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())      
 
167
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())
198
168
            return "?"
199
169
 
200
170
    def getTemplateItemOutput(self, template_text):
201
 
        
 
171
 
202
172
        # keys to template data
203
173
        SERVERTYPE_KEY = "servertype"
204
174
        SERVERNAME_KEY = "servername"
209
179
        PASSWORD_KEY = "password"
210
180
        CONNECTION_TIMEOUT_KEY = "connectiontimeout"
211
181
        MAILINFO_KEY = "mailinfo"
212
 
        
 
182
 
213
183
        servertype = None
214
184
        servername = None
215
185
        port = None
219
189
        password = None
220
190
        connectiontimeout = self.config.CONNECTIONTIMEOUT
221
191
        mailinfo = self.config.MAILINFO
222
 
        
 
192
 
223
193
        for option in template_text.split('--'):
224
194
            if len(option) == 0 or option.isspace():
225
195
                continue
226
 
            
 
196
 
227
197
            # not using split here...it can't assign both key and value in one call, this should be faster
228
198
            x = option.find('=')
229
199
            if (x != -1):
234
204
            else:
235
205
                key = option.strip()
236
206
                value = None
237
 
                    
 
207
 
238
208
            try:
239
209
                if key == SERVERTYPE_KEY:
240
 
                    servertype = self.getTypedValue(value, "string")
 
210
                    servertype = getTypedValue(value, "string")
241
211
                elif key == SERVERNAME_KEY:
242
 
                    servername = self.getTypedValue(value, "string")
 
212
                    servername = getTypedValue(value, "string")
243
213
                elif key == PORT_KEY:
244
 
                    port = self.getTypedValue(value, "integer")                    
 
214
                    port = getTypedValue(value, "integer")
245
215
                elif key == FOLDER_KEY:
246
 
                    folder = self.getTypedValue(value, "string")                    
 
216
                    folder = getTypedValue(value, "string")
247
217
                elif key == SSL_KEY:
248
218
                    ssl = True
249
219
                elif key == USERNAME_KEY:
250
 
                    username = self.getTypedValue(value, "string")
 
220
                    username = getTypedValue(value, "string")
251
221
                elif key == PASSWORD_KEY:
252
 
                    password = self.getTypedValue(value, "string")
 
222
                    password = getTypedValue(value, "string")
253
223
                elif key == CONNECTION_TIMEOUT_KEY:
254
 
                    connectiontimeout = self.getTypedValue(value, "integer")
 
224
                    connectiontimeout = getTypedValue(value, "integer")
255
225
                elif key == MAILINFO_KEY:
256
 
                    mailinfo = self.getTypedValue(value, "integer")
 
226
                    mailinfo = getTypedValue(value, "integer")
257
227
                else:
258
228
                    self.logger.error("Unknown template option: " + option)
259
229
 
260
230
            except (TypeError, ValueError):
261
231
                self.logger.error("Cannot convert option argument to number: " + option)
262
232
                return u""
263
 
                
 
233
 
264
234
        if servername != None:
265
235
            output = self.getOutputData(servertype,servername,port,folder,ssl,username,password,connectiontimeout,mailinfo)
266
 
            output = self.getHTMLText(output)
 
236
            output = getHTMLText(output)
267
237
            return output
268
238
        else:
269
239
            self.logger.error("Template item does not have servername defined")
274
244
        output = u""
275
245
        end = False
276
246
        a = 0
277
 
        
 
247
 
278
248
        # a and b are indexes in the template string
279
249
        # moving from left to right the string is processed
280
250
        # b is index of the opening bracket and a of the closing bracket
281
251
        # everything between b and a is a template that needs to be parsed
282
252
        while not end:
283
253
            b = template.find('[', a)
284
 
            
 
254
 
285
255
            if b == -1:
286
256
                b = len(template)
287
257
                end = True
288
 
            
 
258
 
289
259
            # if there is something between a and b, append it straight to output
290
260
            if b > a:
291
261
                output += template[a : b]
296
266
                    # skip the bracket in the input string and continue from the beginning
297
267
                    a = b + 1
298
268
                    continue
299
 
                    
 
269
 
300
270
            if end:
301
271
                break
302
 
            
 
272
 
303
273
            a = template.find(']', b)
304
 
            
 
274
 
305
275
            if a == -1:
306
276
                self.logger.error("Missing terminal bracket (]) for a template item")
307
277
                return u""
308
 
            
 
278
 
309
279
            # if there is some template text...
310
280
            if a > b + 1:
311
281
                output += self.getTemplateItemOutput(template[b + 1 : a])
312
 
            
 
282
 
313
283
            a = a + 1
314
284
 
315
285
        return output
316
286
 
317
287
    def getEmailData(self,servername,folder,username,num,lines):
318
 
        
 
288
 
319
289
        try:
320
290
            self.logger.info("Processing email data to determine 'From', 'Subject' and 'Received Date'")
321
 
                            
 
291
 
322
292
            sender = None
323
293
            subject = None
324
294
            recvdate = None
325
295
            messageid = None
326
 
            
 
296
 
327
297
            for line in lines:
328
298
                if sender is None and line.find("From: ") >= 0:
329
299
                    text = line.replace("From: ","").strip("\r ")
331
301
                        text = self.decodeHeader(text)
332
302
                    except Exception, e:
333
303
                        sender = text
334
 
                        self.logger.error("getEmailData:Unexpected error when decoding sender:" + e.__str__()+"\n"+traceback.format_exc()) 
 
304
                        self.logger.error("getEmailData:Unexpected error when decoding sender:" + e.__str__()+"\n"+traceback.format_exc())
335
305
                    sender = re.sub('<.*?@.*?>','',text).strip().lstrip('"').rstrip('"') # remove trailing email in <>
336
306
                elif subject is None and line.find("Subject: ") >= 0:
337
307
                    text = line.replace("Subject: ","").strip("\r\" ")
339
309
                        subject = self.decodeHeader(text)
340
310
                    except Exception, e:
341
311
                        subject = text
342
 
                        self.logger.error("getEmailData:Unexpected error when decoding subject:" + e.__str__()+"\n"+traceback.format_exc()) 
 
312
                        self.logger.error("getEmailData:Unexpected error when decoding subject:" + e.__str__()+"\n"+traceback.format_exc())
343
313
                elif recvdate is None and line.find("Date: ") >= 0:
344
314
                    text = line.replace("Date: ","").strip("\r ")
345
315
                    try:
347
317
                        recvdate = datetime.strptime(text,"%d %b %Y %H:%M:%S") # convert to proper datetime
348
318
                    except Exception, e:
349
319
                        recvdate = datetime.now()
350
 
                        self.logger.error("getEmailData:Unexpected error when converting recieve date to datetime:" + e.__str__()+"\n"+traceback.format_exc()) 
 
320
                        self.logger.error("getEmailData:Unexpected error when converting recieve date to datetime:" + e.__str__()+"\n"+traceback.format_exc())
351
321
                elif messageid is None and line.find("Message-ID: ") >= 0:
352
322
                    text = line.replace("Message-ID: ","").strip("\r ")
353
323
                    messageid = text
355
325
                if sender is not None and \
356
326
                   subject is not None and \
357
327
                   recvdate is not None and \
358
 
                   messageid is not None:                    
 
328
                   messageid is not None:
359
329
                    break
360
330
 
361
331
            if subject is None:
362
332
                subject = ""
363
 
            
 
333
 
364
334
            emaildata = EmailData(servername, folder, username, num, sender, subject, recvdate, messageid)
365
 
                
 
335
 
366
336
            return emaildata
367
337
 
368
338
        except Exception, e:
369
 
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())  
 
339
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())
370
340
            return None
371
 
        
 
341
 
372
342
    def getPOPEmailData(self,servername,port,folder,ssl,username,password,mailinfo):
373
343
 
374
344
        try:
375
345
 
376
346
            self.logger.info("Logging on to POP server: "+ servername)
377
 
            
 
347
 
378
348
            if port == None:
379
349
                if ssl == True:
380
350
                    pop = poplib.POP3_SSL(servername)
381
351
                else:
382
 
                    pop = poplib.POP3(servername)            
 
352
                    pop = poplib.POP3(servername)
383
353
            else:
384
354
                if ssl == True:
385
355
                    pop = poplib.POP3_SSL(servername, port)
386
356
                else:
387
 
                    pop = poplib.POP3(servername, port) 
388
 
                    
 
357
                    pop = poplib.POP3(servername, port)
 
358
 
389
359
            pop.user(username)
390
360
            pop.pass_(password)
391
 
            
 
361
 
392
362
            self.logger.info("Getting message count from POP server: "+ servername)
393
 
                            
 
363
 
394
364
            count = len(pop.list()[1])
395
 
            
 
365
 
396
366
            if count > 0 and mailinfo > 0:
397
 
            
 
367
 
398
368
                self.logger.info("Extracting message data from POP server \"%s\""%servername)
399
369
 
400
370
                self.emaillist = []
401
 
                
 
371
 
402
372
                for num in range(count):
403
 
                    
 
373
 
404
374
                    if self.POP_FETCH_OPTION == "TOP":
405
375
                        lines = pop.top(num+1,1)[1]
406
376
                    else:
407
377
                        lines = pop.retr(num+1,1)[1] #more robust but sets message as seen!
408
 
                    
 
378
 
409
379
                    emaildata = self.getEmailData(servername,folder,username,num,lines)
410
 
                    
 
380
 
411
381
                    if emaildata is not None:
412
382
                        self.emaillist.append(emaildata)
413
 
                
 
383
 
414
384
            self.logger.info("Logging off from POP server: "+ servername)
415
 
                
 
385
 
416
386
            pop.quit()
417
 
            
 
387
 
418
388
            return count
419
 
        
 
389
 
420
390
        except Exception, e:
421
391
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())
422
392
            return -1
424
394
    def getIMAPEmailData(self,servername,port,folder,ssl,username,password,mailinfo):
425
395
 
426
396
        try:
427
 
            
 
397
 
428
398
            self.logger.info("Logging on to IMAP server: "+ servername)
429
 
            
 
399
 
430
400
            if port == None:
431
401
                if ssl == True:
432
402
                    imap = imaplib.IMAP4_SSL(servername)
437
407
                    imap = imaplib.IMAP4_SSL(servername, port)
438
408
                else:
439
409
                    imap = imaplib.IMAP4(servername, port)
440
 
                    
 
410
 
441
411
            imap.login(username, password)
442
412
 
443
413
            self.logger.info("Searching for new mail on IMAP server \"%s\" in folder \"%s\""%(servername,folder))
444
 
                
 
414
 
445
415
            imap.select(folder)
446
416
            typ, data = imap.search(None, self.IMAP_SEARCH_OPTION)
447
417
            for item in data:
453
423
                count = (len(nums))
454
424
            else:
455
425
                count = 0
456
 
            
 
426
 
457
427
            if count > 0 and mailinfo > 0:
458
428
 
459
429
                self.logger.info("Extracting message data for IMAP server: "+ servername)
460
 
                
 
430
 
461
431
                self.emaillist = []
462
 
                                
 
432
 
463
433
                for num in nums:
464
434
                    typ, message = imap.fetch(num, '(BODY.PEEK[HEADER])')
465
435
                    lines = message[0][1].split("\n") # grab the content we want and split out lines
466
 
                    
 
436
 
467
437
                    emaildata = self.getEmailData(servername,folder,username,num,lines)
468
 
                    
 
438
 
469
439
                    if emaildata is not None:
470
440
                        self.emaillist.append(emaildata)
471
 
                        
 
441
 
472
442
            self.logger.info("Logging of from IMAP server: "+ servername)
473
 
                    
 
443
 
474
444
            imap.close()
475
445
            imap.logout()
476
446
            imap.shutdown()
477
 
    
 
447
 
478
448
            return count
479
 
    
 
449
 
480
450
        except Exception, e:
481
451
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())
482
452
            return -1
483
 
        
 
453
 
484
454
    def getOutput(self):
485
455
 
486
456
        if self.options.noheader == True:
495
465
        else:
496
466
            headertemplatefilepath = app_path+"/templates/emailheader.template"
497
467
            self.logger.info("Using default header template")
498
 
             
499
 
        #load the file       
 
468
 
 
469
        #load the file
500
470
        try:
501
471
            inputfile = codecs.open(os.path.expanduser(headertemplatefilepath), encoding='utf-8')
502
472
        except Exception, e:
505
475
            headertemplate = inputfile.read()
506
476
        finally:
507
477
            inputfile.close()
508
 
                        
 
478
 
509
479
        if self.options.template != None:
510
480
            templatefilepath = self.options.template
511
481
            self.logger.info("Using custom template file '%s'"%templatefilepath)
525
495
            template = inputfile.read()
526
496
        finally:
527
497
            inputfile.close()
528
 
        
 
498
 
529
499
        output = headertemplate
530
500
        output = output + self.getOutputFromTemplate(template)
531
501
 
532
502
        return output.encode("utf-8")
533
503
 
534
504
    def decodeHeader(self,header_text):
535
 
        
 
505
 
536
506
        text,encoding = decode_header(header_text)[0]
537
507
        if encoding:
538
508
            try:
542
512
        else:
543
513
            return text
544
514
 
545
 
    def getHTMLText(self,text):
546
 
        try:
547
 
            htmlentities = []               
548
 
            for char in text: #html:
549
 
                if ord(char) < 128:
550
 
                    htmlentities.append(char)
551
 
                else:
552
 
                    htmlentities.append('&%s;' % codepoint2name[ord(char)])
553
 
            html = "".join(htmlentities)
554
 
            
555
 
            html = html.replace("\n","<br>\n") # switch out new line for html breaks
556
 
            return html            
557
 
        except:
558
 
            return text
559
 
 
560
 
    def getCleanText(self,html):
561
 
        try:
562
 
            text = str(html)
563
 
            text = text.replace("\n","") # remove new lines from html
564
 
            text = text.replace("&apos;","'") # workaround for shitty xml codes not compliant with html
565
 
            text = text.replace("<br>","\n") # switch out html breaks for new line
566
 
            text = re.sub('<(.|\n)+?>','',text) # remove any html tags
567
 
            text =  re.sub('&(%s);' % '|'.join(name2codepoint), lambda m: chr(name2codepoint[m.group(1)]), text)
568
 
            return text            
569
 
        except:
570
 
            return html
571
 
    
572
 
    def isNumeric(self, string):
573
 
        try:
574
 
            dummy = float(string)
575
 
            return True
576
 
        except:
577
 
            return False
578
 
        
579
515
def getHTML(options):
580
516
    output = Output(options)
581
517
    html = output.getOutput()
584
520
 
585
521
# to enable testing in isolation
586
522
if __name__ == "__main__":
587
 
    
 
523
 
588
524
    parser = OptionParser()
589
 
    parser.add_option("--noheader", dest="noheader", default=False, action="store_true", help=u"Turn off header output. This will override any header template setting to be nothing")        
 
525
    parser.add_option("--noheader", dest="noheader", default=False, action="store_true", help=u"Turn off header output. This will override any header template setting to be nothing")
590
526
    parser.add_option("--headertemplate", dest="headertemplate", type="string", metavar="FILE", help=u"Override the header template for the plugin, default or config based template ignored.")
591
527
    parser.add_option("--template", dest="template", type="string", metavar="FILE", help=u"Override the template for the plugin, default or config based template ignored.")
592
528
    parser.add_option("--verbose", dest="verbose", default=False, action="store_true", help=u"Outputs verbose info to the terminal")
593
529
    parser.add_option("--version", dest="version", default=False, action="store_true", help=u"Displays the version of the script.")
594
 
    parser.add_option("--logfile", dest="logfile", type="string", metavar="FILE", help=u"If a filepath is set, the script logs to the filepath.")                
595
 
    
 
530
    parser.add_option("--logfile", dest="logfile", type="string", metavar="FILE", help=u"If a filepath is set, the script logs to the filepath.")
 
531
 
596
532
    (options, args) = parser.parse_args()
597
 
        
 
533
 
598
534
    output = Output(options)
599
535
    html = output.getOutput()
600
536
    del output