mailcap.py 7.91 KB
Newer Older
1
"""Mailcap file handling.  See RFC 1524."""
Guido van Rossum's avatar
Guido van Rossum committed
2 3

import os
4
import warnings
Guido van Rossum's avatar
Guido van Rossum committed
5

6
__all__ = ["getcaps","findmatch"]
Guido van Rossum's avatar
Guido van Rossum committed
7

8 9 10 11 12 13 14 15 16

def lineno_sort_key(entry):
    # Sort in ascending order, with unspecified entries at the end
    if 'lineno' in entry:
        return 0, entry['lineno']
    else:
        return 1, 0


Guido van Rossum's avatar
Guido van Rossum committed
17 18 19
# Part 1: top-level interface.

def getcaps():
20
    """Return a dictionary containing the mailcap database.
Tim Peters's avatar
Tim Peters committed
21

22 23 24 25 26
    The dictionary maps a MIME type (in all lowercase, e.g. 'text/plain')
    to a list of dictionaries corresponding to mailcap entries.  The list
    collects all the entries for that MIME type from all available mailcap
    files.  Each dictionary contains key-value pairs for that MIME type,
    where the viewing command is stored with the key "view".
27 28

    """
Guido van Rossum's avatar
Guido van Rossum committed
29
    caps = {}
30
    lineno = 0
Guido van Rossum's avatar
Guido van Rossum committed
31
    for mailcap in listmailcapfiles():
32 33
        try:
            fp = open(mailcap, 'r')
34
        except OSError:
35
            continue
36
        with fp:
37
            morecaps, lineno = _readmailcapfile(fp, lineno)
38
        for key, value in morecaps.items():
39
            if not key in caps:
40
                caps[key] = value
41
            else:
42
                caps[key] = caps[key] + value
Guido van Rossum's avatar
Guido van Rossum committed
43 44 45
    return caps

def listmailcapfiles():
46
    """Return a list of all mailcap files found on the system."""
47
    # This is mostly a Unix thing, but we use the OS path separator anyway
48
    if 'MAILCAPS' in os.environ:
49 50
        pathstr = os.environ['MAILCAPS']
        mailcaps = pathstr.split(os.pathsep)
Guido van Rossum's avatar
Guido van Rossum committed
51
    else:
52
        if 'HOME' in os.environ:
53 54 55 56 57 58
            home = os.environ['HOME']
        else:
            # Don't bother with getpwuid()
            home = '.' # Last resort
        mailcaps = [home + '/.mailcap', '/etc/mailcap',
                '/usr/etc/mailcap', '/usr/local/etc/mailcap']
Guido van Rossum's avatar
Guido van Rossum committed
59 60 61 62 63
    return mailcaps


# Part 2: the parser.
def readmailcapfile(fp):
64 65 66 67 68 69 70 71
    """Read a mailcap file and return a dictionary keyed by MIME type."""
    warnings.warn('readmailcapfile is deprecated, use getcaps instead',
                  DeprecationWarning, 2)
    caps, _ = _readmailcapfile(fp, None)
    return caps


def _readmailcapfile(fp, lineno):
72 73 74 75 76 77 78 79
    """Read a mailcap file and return a dictionary keyed by MIME type.

    Each MIME type is mapped to an entry consisting of a list of
    dictionaries; the list will contain more than one such dictionary
    if a given MIME type appears more than once in the mailcap file.
    Each dictionary contains key-value pairs for that MIME type, where
    the viewing command is stored with the key "view".
    """
Guido van Rossum's avatar
Guido van Rossum committed
80 81
    caps = {}
    while 1:
82 83 84
        line = fp.readline()
        if not line: break
        # Ignore comments and blank lines
85
        if line[0] == '#' or line.strip() == '':
86 87 88 89 90 91 92 93 94 95 96
            continue
        nextline = line
        # Join continuation lines
        while nextline[-2:] == '\\\n':
            nextline = fp.readline()
            if not nextline: nextline = '\n'
            line = line[:-2] + nextline
        # Parse the line
        key, fields = parseline(line)
        if not (key and fields):
            continue
97 98 99
        if lineno is not None:
            fields['lineno'] = lineno
            lineno += 1
100
        # Normalize the key
101
        types = key.split('/')
102
        for j in range(len(types)):
103 104
            types[j] = types[j].strip()
        key = '/'.join(types).lower()
105
        # Update the database
106
        if key in caps:
107 108 109
            caps[key].append(fields)
        else:
            caps[key] = [fields]
110
    return caps, lineno
Guido van Rossum's avatar
Guido van Rossum committed
111 112

def parseline(line):
113 114 115 116 117
    """Parse one entry in a mailcap file and return a dictionary.

    The viewing command is stored as the value with the key "view",
    and the rest of the fields produce key-value pairs in the dict.
    """
Guido van Rossum's avatar
Guido van Rossum committed
118 119 120
    fields = []
    i, n = 0, len(line)
    while i < n:
121 122 123
        field, i = parsefield(line, i, n)
        fields.append(field)
        i = i+1 # Skip semicolon
Guido van Rossum's avatar
Guido van Rossum committed
124
    if len(fields) < 2:
125
        return None, None
Guido van Rossum's avatar
Guido van Rossum committed
126 127 128
    key, view, rest = fields[0], fields[1], fields[2:]
    fields = {'view': view}
    for field in rest:
129
        i = field.find('=')
130 131 132 133
        if i < 0:
            fkey = field
            fvalue = ""
        else:
134 135
            fkey = field[:i].strip()
            fvalue = field[i+1:].strip()
136
        if fkey in fields:
137 138 139 140
            # Ignore it
            pass
        else:
            fields[fkey] = fvalue
Guido van Rossum's avatar
Guido van Rossum committed
141 142 143
    return key, fields

def parsefield(line, i, n):
144
    """Separate one key-value pair in a mailcap entry."""
Guido van Rossum's avatar
Guido van Rossum committed
145 146
    start = i
    while i < n:
147 148 149 150 151 152 153
        c = line[i]
        if c == ';':
            break
        elif c == '\\':
            i = i+2
        else:
            i = i+1
154
    return line[start:i].strip(), i
Guido van Rossum's avatar
Guido van Rossum committed
155 156 157 158


# Part 3: using the database.

159 160
def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
    """Find a match for a mailcap entry.
Tim Peters's avatar
Tim Peters committed
161

162 163 164 165 166 167 168
    Return a tuple containing the command line, and the mailcap entry
    used; (None, None) if no match is found.  This may invoke the
    'test' command of several matching entries before deciding which
    entry to use.

    """
    entries = lookup(caps, MIMEtype, key)
Tim Peters's avatar
Tim Peters committed
169
    # XXX This code should somehow check for the needsterminal flag.
Guido van Rossum's avatar
Guido van Rossum committed
170
    for e in entries:
171
        if 'test' in e:
172 173 174 175 176
            test = subst(e['test'], filename, plist)
            if test and os.system(test) != 0:
                continue
        command = subst(e[key], MIMEtype, filename, plist)
        return command, e
Guido van Rossum's avatar
Guido van Rossum committed
177 178
    return None, None

179
def lookup(caps, MIMEtype, key=None):
Guido van Rossum's avatar
Guido van Rossum committed
180
    entries = []
181
    if MIMEtype in caps:
182
        entries = entries + caps[MIMEtype]
183
    MIMEtypes = MIMEtype.split('/')
184
    MIMEtype = MIMEtypes[0] + '/*'
185
    if MIMEtype in caps:
186
        entries = entries + caps[MIMEtype]
Guido van Rossum's avatar
Guido van Rossum committed
187
    if key is not None:
188
        entries = [e for e in entries if key in e]
189
    entries = sorted(entries, key=lineno_sort_key)
Guido van Rossum's avatar
Guido van Rossum committed
190 191
    return entries

192
def subst(field, MIMEtype, filename, plist=[]):
Guido van Rossum's avatar
Guido van Rossum committed
193 194 195 196
    # XXX Actually, this is Unix-specific
    res = ''
    i, n = 0, len(field)
    while i < n:
197
        c = field[i]; i = i+1
198
        if c != '%':
199 200 201 202 203 204 205 206 207 208 209 210 211
            if c == '\\':
                c = field[i:i+1]; i = i+1
            res = res + c
        else:
            c = field[i]; i = i+1
            if c == '%':
                res = res + c
            elif c == 's':
                res = res + filename
            elif c == 't':
                res = res + MIMEtype
            elif c == '{':
                start = i
212
                while i < n and field[i] != '}':
213 214 215 216 217 218 219 220 221
                    i = i+1
                name = field[start:i]
                i = i+1
                res = res + findparam(name, plist)
            # XXX To do:
            # %n == number of parts if type is multipart/*
            # %F == list of alternating type and filename for parts
            else:
                res = res + '%' + c
Guido van Rossum's avatar
Guido van Rossum committed
222 223 224
    return res

def findparam(name, plist):
225
    name = name.lower() + '='
Guido van Rossum's avatar
Guido van Rossum committed
226 227
    n = len(name)
    for p in plist:
228
        if p[:n].lower() == name:
229
            return p[n:]
Guido van Rossum's avatar
Guido van Rossum committed
230 231 232 233 234 235 236 237 238
    return ''


# Part 4: test program.

def test():
    import sys
    caps = getcaps()
    if not sys.argv[1:]:
239 240
        show(caps)
        return
Guido van Rossum's avatar
Guido van Rossum committed
241
    for i in range(1, len(sys.argv), 2):
242 243
        args = sys.argv[i:i+2]
        if len(args) < 2:
244
            print("usage: mailcap [MIMEtype file] ...")
245 246 247 248 249
            return
        MIMEtype = args[0]
        file = args[1]
        command, e = findmatch(caps, MIMEtype, 'view', file)
        if not command:
250
            print("No viewer found for", type)
251
        else:
252
            print("Executing:", command)
253 254
            sts = os.system(command)
            if sts:
255
                print("Exit status:", sts)
Guido van Rossum's avatar
Guido van Rossum committed
256 257

def show(caps):
258 259 260
    print("Mailcap files:")
    for fn in listmailcapfiles(): print("\t" + fn)
    print()
Guido van Rossum's avatar
Guido van Rossum committed
261
    if not caps: caps = getcaps()
262 263
    print("Mailcap entries:")
    print()
264
    ckeys = sorted(caps)
Guido van Rossum's avatar
Guido van Rossum committed
265
    for type in ckeys:
266
        print(type)
267 268
        entries = caps[type]
        for e in entries:
269
            keys = sorted(e)
270
            for k in keys:
271 272
                print("  %-15s" % k, e[k])
            print()
Guido van Rossum's avatar
Guido van Rossum committed
273 274 275

if __name__ == '__main__':
    test()