1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
import os
import subprocess
import confparser
import imapclient
import contextlib
import backports.ssl as ssl
import socket
import time
def conf_boolean_postprocess(src):
src = src.lower().strip()
if src == 'true' or src == 'on' or src == '1':
return True
return False
def conf_postprocess(conf):
d = {
'imap.tls': conf_boolean_postprocess,
'imap.start_tls': conf_boolean_postprocess,
'imap.tls_nocheck_hostname': conf_boolean_postprocess,
'imap.tls_nocheck_ca': conf_boolean_postprocess,
}
return {key: d[key](value) if key in d else value
for key, value in conf.items()}
@contextlib.contextmanager
def connect_to_imap(conf, password):
cafile = conf.get('imap.tls_ca', None)
if cafile:
cafile = os.path.expanduser(cafile)
ssl_context = imapclient.create_default_context(cafile=cafile)
if conf.get('imap.tls_nocheck_hostname', False):
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
if conf.get('imap.tls_nocheck_ca', False):
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
connection = imapclient.IMAPClient(host=conf.get('imap.server'),
ssl=conf.get('imap.tls', True),
ssl_context=ssl_context,
use_uid=True)
if conf.get('imap.start_tls', False):
connection.start_tls(ssl_context=ssl_context)
print('connection succeed')
connection.login(username=conf.get('imap.username'), password=password)
print('successfuly logged')
# never try to shutdown socket or send a logout since the goal of climl is
# to stay connected, if this fails it is an error and thus no need to try
# to be nice with a logout to a dead connection.
yield connection
def imap_waiter(connection):
'''
from dovecot wiki:
> If IDLE command is started, Dovecot never
> disconnects. Only if the connection is lost there will be
> a disconnection. A dead connection is detected by Dovecot
> periodically sending "I'm still here" notifications to
> client (imap_idle_notify_interval setting - default every
> 2 minutes).
>
> IMAP clients are supposed to send something before 30
> minutes are up, but several clients don't do this. Some
> Outlook versions even stop receiving new mails entirely
> until manual intervention if IMAP server disconnects the
> client.
We are a compliant client. To be compatible with a lot of servers
(general assumption is a 29minutes timeout), we try to redo idle-mode
every 24 minutes. Each call to imap_waiter is a single idle iteration.
Thus this function may return empty list.
Note: on some servers, there might be events between the last search
command and the begining of idle mode. If those events represent new
mails, they will be checked at the idle timeout (it takes time to see
them, but they are not lost).
'''
print('entering idle mode...')
connection.idle()
print('waiting for an event')
try:
tenter = time.monotonic()
while (time.monotonic() - tenter) <= 24*60:
events = connection.idle_check(timeout=24*60)
if len(events) == 1 and \
str(bytes(events[0][0]), encoding='ascii') == 'OK':
# in case of dovecot 'Ok still here' keepalives, timeout did
# not expired (and never will appart from a disconnection)
continue
else:
break
except KeyboardInterrupt:
print('quitting idle mode on interrupt.')
connection.idle_done()
raise
print('quitting idle mode...')
command_text, idle_responses = connection.idle_done()
events.extend(idle_responses)
print('idle_done results:', command_text, idle_responses)
return events
def main(callback=None):
assert callback
confpath = os.path.expanduser('~/.config/climl/climl.cfg')
conf = conf_postprocess(confparser.read_conf(confpath))
print('Read conf:', conf)
password_command = conf.get('imap.password_command', None)
if password_command:
password = subprocess.check_output(password_command, shell=True)
password = password.rstrip().decode('utf8')
print('got pasword:', password)
maxsize = conf.get('mail.maxsize', 100*1024) # 100k default
maxsize = int(maxsize)
while True:
print('connection...')
try:
with connect_to_imap(conf, password) as connection:
print('selecting folder', conf.get('imap.mailbox'))
connection.select_folder(conf.get('imap.mailbox'))
# at start, select all mails
idlist = connection.search(['UNSEEN'])
print('initial idlist: ', idlist)
for oneid in idlist:
print('new mail:', oneid)
print('checking size of mail...')
response = connection.fetch([oneid], ['RFC822.SIZE'])
size = int(response[oneid][b'RFC822.SIZE'])
print('message size: {} bytes'.format(size))
if size > maxsize:
print('message is too big, skip and mark it seen')
connection.add_flags([oneid], ['\Seen'])
continue
print('getting mail...')
response = connection.fetch([oneid], ['RFC822'])
data = response[oneid][b'RFC822']
print('calling callback...')
callback('mail: ' + str(oneid) + ': ' + str(data))
data = None
while True:
imap_waiter(connection)
idlist = connection.search(['UNSEEN'])
for oneid in idlist:
print('calling callback...')
callback('mail: ' + str(oneid))
except (socket.error,
socket.timeout,
ssl.SSLError,
ssl.CertificateError):
print('socket/ssl error, retrying in 10s...')
try:
time.sleep(10) # wait between retries
except KeyboardInterrupt:
break
except KeyboardInterrupt:
break
print('end of connection')
|