wimap

Check-in [781ec6c7e9]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:First pass, iMap web interface
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | descendants | master | trunk
Files: files | file ages | folders
SHA3-256:781ec6c7e901242319457f07b7f63b413e56f0276f57d284fbeebb8d7a92f1fd
User & Date: ajv-899-334-8894@vsta.org 2016-11-26 23:23:36
Context
2016-11-27
01:17
Forward and back in message lists. Fix noop check of server status. Emphasis on date, don't use space for message index. check-in: 17e03cf724 user: ajv-899-334-8894@vsta.org tags: master, trunk
2016-11-26
23:23
First pass, iMap web interface check-in: 781ec6c7e9 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace

Added .gitignore.







>
>
>
1
2
3
*.pyc
certs
chore

Added etc/.gitignore.



>
1
*.real

Added etc/config.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
service "iMap Reader"

# Plain old web interface
serve http
    port 8084
    iface eth0
# Crypto to the world
serve https
    port 8094
    publicCert certs/signed.crt
    privateKey certs/domain.key

Added get.py.



























































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
166
167
168
169
170
171
172
173
#
# get.py
#	Mixin to implement HTML GET operations
#
# Structure of paths in the server:
#  /
#	Main UI.  Top part is playlist, bottom is file/dir browser
#  /media/<prefix>/...
#	For each prefix of files, its contents is served by
#	way of its path under here.
import sys, urllib, cgi, time
from email.utils import parseaddr
import imap

# How many messages to list at a time
NMSG = 20

# The GET part of our handling
class GET_mixin(object):

    # Configure our WPlayer GET treatment
    def __init__(self):

        # Server connection
        self.srv = None

	# GET handlers
	self.dispatchers.append( ("GET", self.list_folder) )
	self.dispatchers.append( ("GET", self.read_msg) )

    # Get server connection if needed
    def get_server(self):
	server = self.server
	approot = server.approot

        srv = self.srv
        if srv is not None:
            return srv
        user = self.user
        sys.stderr.write("uconfig %r\n" % (approot.uconfig,))
        self.srv = srv = imap.get(user,
            approot.uconfig.get(user),
            approot.imaps)
        return srv

    # "/": list folders
    def send_top(self):

        # Get an imap server connection.  It could be cached,
        #  or it might be set up right now.
        srv = self.get_server()
        if srv is None:
            # No imap connection available
            self.send_error(503)
            return True,None

        buf = self.build_header("Folders")
        buf += '<h2>Select folder:</h2>\n'
        for f in srv.folders():
            buf += ' <a href="/%s">%s</a><br>\n' % \
             (urllib.quote_plus(f), cgi.escape(f))

        buf = self.build_tailer(buf)

        return self.send_result(buf, "text/html")

    # Valid folder name.
    # Starts with alpha, and then alnum or '/', and with alnum
    def valid_fname(self, fn):
        # Give me a break
        if not fn:
            return False

        # Starts with letter, ands with alnum
        if (not fn[0].isalpha()) or (not fn[-1].isalnum()):
            return False

        # No empty elements between slashes
        tup = fn.split('/')
        if any((not s) for s in tup):
            return False

        # All alnum
        if any((not s.isalnum()) for s in tup):
            return False

        return True

    # See if this names a folder, then list contents
    def list_folder(self):
        pp = self.paths
        if len(pp) != 1:
            return False,None

        # Which folder?
        fn = urllib.unquote_plus(pp[0])
        if not self.valid_fname(fn):
            return False,None

        # Start at which index number?
        if self.vals and ("from" in self.vals):
            try:
                msgidx = int(self.vals["from"])
            except:
                return False,None
        else:
            msgidx = 1

        # Ask our library to pull them together
        srv = self.get_server()
        msgs = srv.messages(fn, msgidx, NMSG)
        if msgs is None:
            return False,None

        # Here's your messages
        buf = self.build_header("Messages %d-%d" %
            (msgidx, msgidx+len(msgs)-1))
        buf += '<table>\n'
        for msgidx,flags,fields in msgs:
            buf += ' <tr>\n'

            # Message #, emphasized if not marked as read
            if "Seen" not in flags:
                buf += '  <td><em>%d</em></td>\n' % (msgidx,)
            else:
                buf += '  <td>%d</td>\n' % (msgidx,)

            # Time if today
            # mm/dd if this year
            # mm/yy if previous years
            today = time.localtime()
            tm = srv.dt_parse(fields["date"])

            # This year
            if tm.tm_year == today.tm_year:
                # Today
                if (tm.tm_mon == today.tm_mon) and \
                        (tm.tm_mday == today.tm_mday):
                    buf += '  <td>%02d:%02d</td>\n' % \
                        (tm.tm_hour, tm.tm_min)
                else:
                    # Some month of this year
                    buf += '  <td>%d/%d</td>\n' % \
                        (tm.tm_mon, tm.tm_mday)
            else:
                # Some month of that year
                buf += '  <td>%d/%04d</td>\n' % \
                    (tm.tm_mon, tm.tm_year)

            # Who
            if "from" in fields:
                tup = parseaddr(fields["from"])
                s = tup[0].strip() or tup[1].strip() or "--"
                s = s[:20]
                buf += '  <td>%s</td>\n' % (cgi.escape(s),)
            else:
                buf += '  <td>--</td>\n'

            # Subject
            buf += '  <td>%s</td>\n' % \
             (cgi.escape(fields.get("subject", "--")),)

            buf += ' </tr>\n'

        buf += '</table>\n'

        buf = self.build_tailer(buf)

        return True,self.send_result(buf, "text/html")

    # Message number under folder, render contents
    def read_msg(self):
        return None

Added imap.py.













































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#
# imap.py
#       imaplib services
#
import imaplib, time, sys
from email.header import decode_header

# Wrapper around imaplib connection
class Server(object):
    def __init__(self, user, srv):
        self.user = user
        self.srv = srv

    # Get list of folders
    def folders(self):
        srv = self.srv

        tup = srv.list()
        if tup[0] != "OK":
            sys.stderr.write("%s list failed: %s\n" %
                (self.user, tup[1]))
            return ()
        folders = tup[1]

        names = set()
        for s in folders:
            # '(flags...) "/" <name>'
            tup = s.split('"/"')
            if len(tup) != 2:
                sys.stderr.write("Unexpected folder: %s\n" % (s,))
                continue

            if "Noselect" in tup[0]:
                # Not an actual folder with messages
                continue

            names.add(tup[1].strip())
        return sorted(names)

    # Parse mail file style fields, where continuations
    #  are done with leading tabs of following lines
    # Return a list of lines, one entire field per entry
    def collect_fields(self, fields):
        res = []

        for f in fields.replace('\r', '').split('\n'):
            # Empty, ignore
            if not f:
                continue

            # New field
            if f[0] != '\t':
                res.append(f)
                continue

            # Continuation
            assert res
            res[-1] += f[1:]

        return res

    # Turn this selection of message fields into a dict
    def field_dict(self, fields):
        res = {}
        fields = self.collect_fields(fields)
        for f in fields:
            if not f:
                continue
            if ": " not in f:
                continue
            idx = f.index(": ")
            v = f[idx+2:]

            # For encoded (typically "q code") headers
            dv = decode_header(v)
            if not dv:
                dv = v
            else:
                dv = dv[0][0]
            res[f[:idx]] = res[f[:idx].lower()] = dv
        return res

    # Convert imap message flags to set
    def flags_set(self, flags):
        if "FLAGS (" not in flags:
            return ()

        # "FLAGS (\\Flag1 \\Flag2...) ..."
        # -> "\\Flag1 \\Flag2.."
        idx = flags.index("FLAGS (")
        flags = flags[idx+7:]
        if ')' not in flags:
            return ()
        idx = flags.index(')')
        flags = flags[:idx]

        # Burst flags, insert simple name in set
        res = set()
        for f in flags.split():
            res.add(f[1:])

        return res

    # Parse an email Date: field as a timeval
    def dt_parse(self, t):

        # Normalize whitespace
        tup = t.split()
        base = " ".join(tup[:5])

        # Try to interpret by the RFC
        try:
            tm = time.strptime(base, "%a, %d %b %Y %H:%M:%S")
            tmf = time.mktime(tm)
            hrs = tup[5]
            if hrs[0].isalpha():
                # TBD, decode timezone name like "EST" and get
                #  an offset.  But standard Python doesn't
                #  appear to have this?!?
                pass
            else:
                deltaval = 60*60*int(hrs[1:3]) + 60*int(hrs[3:5])
                if hrs.startswith('+'):
                    tmf += deltaval
                elif hrs.startswith('-'):
                    tmf -= deltaval

        # Just sigh and use "now" if it's malformed
        except:
            tmf = time.time()

        return time.localtime(tmf)

    # Return list of metadata on messages starting at the
    #  given index for @nmsg
    def messages(self, fn, msgidx, maxmsg):
        srv = self.srv

        # Select folder, get message count
        tup = srv.select(fn, readonly=True)
        if tup[0] != "OK":
            return None
        nmsg = int(tup[1][0])

        # Walk messages, generating metadata for each message
        res = []
        msgcount = 0
        while msgidx <= nmsg:
            # Message header
            tup = srv.fetch(msgidx,
             '(FLAGS BODY[HEADER.FIELDS (FROM TO DATE STATUS SUBJECT)])')
            if tup[0] != "OK":
                sys.stderr.write("%s msg %d failed: %s\n" %
                    (self.user, msgidx, tup[1]))
                break

            # Encode it for our convenience
            flags = self.flags_set(tup[1][0][0])
            fields = self.field_dict(tup[1][0][1])

            # Add to results
            res.append( (msgidx, flags, fields) )

            # On to next
            msgidx += 1
            msgcount += 1
            if msgcount >= maxmsg:
                break

        return res

# Get or start an imap server connection
def get(user, uconfig, imaps):
    # Existing connection?
    srv = imaps.get(user)
    if srv is not None:
        # Make sure it's still alive
        import pdb
        pdb.set_trace()
        tup = srv.noop()
        if tup[0] == "OK":
            return srv
        srv.logout()
        del imaps[user]

    # New connection
    server = uconfig.get("server")
    passw = uconfig.get("pass")
    if "user" in uconfig:
        uname = uconfig["user"]
    else:
        uname = user
    if (not server) or (not passw):
        return None
    try:
        srv = imaplib.IMAP4_SSL(server)
    except:
        srv = None
    if srv is None:
        # Can't reach server
        return None

    # Authenticate
    tup = srv.login(uname, passw)
    if tup[0] != "OK":
        sys.stderr.write("%s imap login failed: %s\n" %
            (user, tup[1]))
        srv.logout()
        return None

    # We're on.  Wrap the imaplib connection in our own state
    srv = Server(user, srv)
    imaps[user] = srv
    return srv

Added main.py.























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#
# main.py
#	Main driver for ePub web reader
#
import sys, time
import chore
from get import GET_mixin
from chore.authen import Authen_mixin, Authen_Server_mixin

# Tie our various handlers together
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
        Authen_mixin):
    def __init__(self, conn, tup, approot):

	chore.handlers.Chore_Handler.__init__(self, conn, tup, approot,
	    (GET_mixin.__init__,
             Authen_mixin.__init__))

# Load our configuration file
def load_cfg(fn):

    # A configurator
    c = chore.config.Config()

    # Let the web network side add its config entries
    chore.www.add_config(c)

    # Parse the input
    return c.load_cfg(fn)

# Root of our app server
class App(chore.server.Server, Authen_Server_mixin):

    def __init__(self, config):
	# Let Chore handle most things
	chore.server.Server.__init__(self, config, App_Handler);

        # Cached imap server connections
        self.imaps = {}

        # Use authentication server
        Authen_Server_mixin.__init__(self)
        self.authentication.append(Authen_mixin.auth_server)
        self.init_acct_server()

if __name__ == "__main__":
    if len(sys.argv) != 2:
	sys.stderr.write("Usage is: %s <config-file>\n" %
	    (sys.argv[0],))
	sys.exit(1)

    # Create the server with config
    app = App(load_cfg(sys.argv[1]))

    # It's an HTTP service
    app.start_http()

    # HTTP servers each get their own thread.
    sys.exit(0)