Catch the exception if decoding failed.
[pymailheaders.git] / imapprl.py
blob8cd9f7420accc6d83a64722c7295a0a581faff91
1 #!/usr/bin/env python
3 # imapprl.py
4 # Copyright 2007 Neil Shi <zeegeek@gmail.com>
6 # IMAP Protocol
7 # This is the IMAP protocol support module for pymailheaders.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 import imaplib
20 import socket
21 import re
22 from email.Header import decode_header
24 import chardet
25 from exception import *
27 class imap:
28 """imap class
30 @attention: if an exception Error is thrown by any of the method, by
31 disconnecting and connecting again, the problem should be solved.
33 @warning: B{Have to call connect() method before doing anything else}
35 @note: Private member variables:
36 __TIMEOUT
37 __server
38 __mbox
39 __uname
40 __pass
41 __ssl
42 __connection
43 __size
44 """
46 __TIMEOUT = 20
48 def __init__(self, server, uname, password, ssl, h, mbox):
49 """Constructor
51 @type server: string
52 @param server: mail server address
53 @type uname: string
54 @param uname: username
55 @type password: string
56 @param password: password
57 @type ssl: bool
58 @param ssl: if this is a secure connection
59 @type h: int
60 @param h: number of messages displayable in the window
61 @type mbox: string
62 @param mbox: mailbox.
63 """
65 self.__server = server
66 self.__mbox = mbox
67 self.__uname = uname
68 self.__pass = password
69 self.__ssl = ssl
70 self.__size = h
72 def __del__(self):
73 """Destructor
74 Should log out and destroy the connection.
75 """
77 try:
78 response = self.__connection.logout()
79 if response[0] != 'BYE':
80 raise Error('imapprl (__del__)', response[1])
81 except (socket.error, socket.gaierror, imaplib.IMAP4.error,
82 imaplib.IMAP4.abort), strerr:
83 raise Error('imapprl (__del__)', str(strerr))
84 except:
85 raise
87 def __check(self):
88 """Get the total number and the number of new messages in a mailbox.
90 @rtype: tuple
91 @return: (total number of messages, number of new messages)
92 """
94 try:
95 response = self.__connection.status('INBOX', '(MESSAGES UNSEEN)')
96 if response[0] != 'OK':
97 raise Error('imapprl (__check)', response[1])
98 except (socket.error, socket.gaierror, imaplib.IMAP4.error,
99 imaplib.IMAP4.abort), strerr:
100 raise Error('imapprl (__check)', str(strerr))
101 except:
102 raise
104 num = re.search('\D+(\d+)\D+(\d+)', response[1][0]).groups()
105 return (int(num[0]), int(num[1]))
107 def __select_mailbox(self):
108 """Select a mailbox
111 try:
112 response = self.__connection.select(self.__mbox, True)
113 if response[0] != 'OK':
114 raise Error('imapprl (__select_mailbox)', response[1])
115 except (socket.error, socket.gaierror, imaplib.IMAP4.error,
116 imaplib.IMAP4.abort), strerr:
117 raise Error('imapprl (__select_mailbox)', str(strerr))
118 except:
119 raise
121 def connect(self):
122 """Connect to the server and log in.
124 If the connection has already established, return.
126 @attention: when exception TryAgain is thrown by this method,
127 the calling program should try to connect again.
129 @raise TryAgain: when network is temporarily unavailable
132 try:
133 if self.__ssl:
134 self.__connection = imaplib.IMAP4_SSL(self.__server)
135 else:
136 self.__connection = imaplib.IMAP4(self.__server)
137 self.__connection.socket().settimeout(self.__TIMEOUT)
139 response = self.__connection.login(self.__uname, self.__pass)
140 if response[0] != 'OK':
141 raise Error('imapprl (connect)', response[1])
142 except socket.gaierror, (socket.EAI_AGAIN, strerr):
143 raise TryAgain('imapprl (connect)', strerr)
144 except (socket.error, socket.gaierror, imaplib.IMAP4.error), strerr:
145 raise Error('imapprl (connect)', str(strerr))
146 except:
147 raise
149 def get_mail(self):
150 """Get mails.
152 @rtype: list
153 @return: List of tuples of flag, sender addresses and subjects.
154 newest message on top.
156 @note: flag I{B{True}} for new messages
159 try:
160 num = self.__check()
161 self.__select_mailbox()
163 # if the number of new messages is more than what the window can
164 # hold, get them all. Otherwise, fill up the whole window with old
165 # messages at the bottom.
166 if self.__size < num[1]:
167 num_to_fetch = str(num[0] - num[1])
168 else:
169 num_to_fetch = str(num[0] < self.__size and 1 \
170 or num[0] - self.__size)
171 mail_list = self.__connection.fetch(num_to_fetch + ':' + \
172 str(num[0]), '(FLAGS BODY.PEEK' \
173 + '[HEADER.FIELDS ' \
174 + '(FROM SUBJECT)])')
175 if mail_list[0] != 'OK':
176 raise Error('imapprl (get_mail)', response[1])
178 response = self.__connection.close()
179 if response[0] != 'OK':
180 raise Error('imapprl (get_mail)', response[1])
181 except (socket.error, socket.gaierror, imaplib.IMAP4.error,
182 imaplib.IMAP4.abort), strerr:
183 raise Error('imapprl (get_mail)', str(strerr))
184 except:
185 raise
187 def d(x):
188 # In case the string is not compliant with the standard, let's make
189 # it correct.
190 try:
191 y = decode_header(re.sub(r'(=\?([^\?]*\?){3}=)', r' \1 ', x))
192 return ''.join(s[1] and s[0].decode(s[1]) or s[0] for s in y)
193 except UnicodeDecodeError:
194 raise Error('imapprl (get_mail)', 'Invalid encoding')
196 # parse sender addresses and subjects
197 def a(x): return x != ')'
198 # ATTENTION: cannot rely on the order of the reply by fetch
199 # command, it's arbitrary.
200 def b(x):
201 sender = re.search('From: ([^\r\n]+)', x[1].strip()).group(1)
202 # get sender's name if there's one, otherwise get the email address
203 (name, addr) = re.search('("?([^"]*)"?\s)?<?(([a-zA-Z0-9_\-\.])+@(([0-2]?[0-5]?[0-5]\.[0-2]?[0-5]?[0-5]\.[0-2]?[0-5]?[0-5]\.[0-2]?[0-5]?[0-5])|((([a-zA-Z0-9\-])+\.)+([a-zA-Z\-])+)))?>?', sender).groups()[1:3]
204 subject = re.search('Subject:\s*((.*\s*)+)', x[1].strip())
205 # subject might be empty
206 if subject == None:
207 subject = ''
208 else:
209 subject = re.sub('\r?\n?', '', subject.group(1))
210 # ATTENTION: some mail agents will clear all the flags to indicate
211 # that a message is unread
212 return (re.search('FLAGS \(.*\\Seen.*\)', \
213 x[0].strip()) == None, \
214 name and d(name.strip()) or addr, \
215 d(subject))
216 messages = map(b, filter(a, mail_list[1]))
217 messages.reverse()
218 return messages