2019-07-03 21:39:38 +00:00
|
|
|
// Package mailcap is a port of the python3 mailcap library to golang and
|
|
|
|
// provides developers with the ability to query mimetypes and keys against
|
|
|
|
// the various mailcap files on a system to acquire an appropriate command
|
|
|
|
// to run in order to view, edit, etc a given file of a given mimetype.
|
2019-06-28 04:13:55 +00:00
|
|
|
package mailcap
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
2019-06-29 05:51:20 +00:00
|
|
|
"os/exec"
|
2019-06-28 04:13:55 +00:00
|
|
|
"strings"
|
2019-06-29 05:51:20 +00:00
|
|
|
"strconv"
|
2019-06-28 04:13:55 +00:00
|
|
|
"fmt"
|
|
|
|
"bufio"
|
2019-06-29 05:51:20 +00:00
|
|
|
"sort"
|
2019-06-28 04:13:55 +00:00
|
|
|
)
|
|
|
|
|
2019-07-03 21:39:38 +00:00
|
|
|
// Entry is a map of strings keyed with strings that represents a single
|
|
|
|
// mailcap entry. It represents one prgram for one mimetype. An entry can
|
|
|
|
// contain options for various keys (test, view, edit, etc).
|
2019-06-28 04:13:55 +00:00
|
|
|
type Entry map[string]string
|
2019-07-03 21:39:38 +00:00
|
|
|
|
|
|
|
// Mailcap is the main struct to interact with. It contains a Cap, as
|
|
|
|
// caps, that represents the full mailcap db for the system. It has a
|
|
|
|
// number of receivers to facilitate retrieving a command from a mimetype.
|
2019-06-28 04:13:55 +00:00
|
|
|
type Mailcap struct {
|
2019-07-06 21:40:29 +00:00
|
|
|
DB map[string][]Entry
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
|
2019-06-29 20:37:56 +00:00
|
|
|
// Creates and initializes the mailcap struct/db
|
2019-07-03 18:21:39 +00:00
|
|
|
// and returns a pointer to Mailcap struct
|
2019-06-28 04:13:55 +00:00
|
|
|
func NewMailcap() *Mailcap {
|
2019-07-06 21:40:29 +00:00
|
|
|
mc := Mailcap{make(map[string][]Entry)}
|
2019-06-28 04:13:55 +00:00
|
|
|
mc.getCaps()
|
|
|
|
return &mc
|
|
|
|
}
|
|
|
|
|
2019-07-06 21:40:29 +00:00
|
|
|
// Returns []Entry for a given mimetype and an error or nil.
|
|
|
|
func (m *Mailcap) GetAllMime(mime string) ([]Entry, error) {
|
|
|
|
if v, ok := m.DB[mime]; ok {
|
2019-06-29 20:37:56 +00:00
|
|
|
return v, nil
|
|
|
|
}
|
2019-07-06 21:40:29 +00:00
|
|
|
return nil, fmt.Errorf("Cannot find %s\n", mime)
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
|
2019-07-03 18:21:39 +00:00
|
|
|
// Retrieves an Entry for a given mimetype and key.
|
|
|
|
// Key is required to make sure the returned Entry has
|
|
|
|
// the capability that is desired. Common keys: "view", "edit".
|
2019-07-03 19:30:49 +00:00
|
|
|
// Also takes a bool, needsTerm, that can be set to true if the
|
|
|
|
// command must be executable in a terminal only environment.
|
|
|
|
//
|
2019-07-03 18:21:39 +00:00
|
|
|
// Returns and Entry and an error
|
2019-07-03 19:30:49 +00:00
|
|
|
func (m *Mailcap) FindMatch(mime, key string, needsTerm bool) (Entry, error) {
|
2019-06-29 05:51:20 +00:00
|
|
|
entries := m.lookup(mime, key)
|
|
|
|
for _, v := range entries {
|
|
|
|
exitCode := 0;
|
2019-07-03 19:30:49 +00:00
|
|
|
if _, ok := v["needsterminal"]; needsTerm && !ok {
|
|
|
|
continue
|
|
|
|
}
|
2019-06-29 05:51:20 +00:00
|
|
|
if t, ok := v["test"]; ok {
|
|
|
|
cmdArgs := strings.Split(t, " ")
|
|
|
|
if len(cmdArgs) < 2 {
|
|
|
|
continue
|
|
|
|
}
|
2019-06-29 20:37:56 +00:00
|
|
|
|
2019-06-29 05:51:20 +00:00
|
|
|
com := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
|
|
|
if err := com.Run(); err != nil {
|
|
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
|
|
exitCode = exitError.ExitCode()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if exitCode != 0 {
|
|
|
|
continue
|
|
|
|
}
|
2019-07-03 19:30:49 +00:00
|
|
|
v["action"] = key
|
2019-07-03 18:21:39 +00:00
|
|
|
return v, nil
|
2019-06-29 05:51:20 +00:00
|
|
|
}
|
|
|
|
|
2019-07-06 21:40:29 +00:00
|
|
|
return nil, fmt.Errorf("Unable to find key %q in entries for %s", key, mime)
|
2019-06-29 05:51:20 +00:00
|
|
|
}
|
|
|
|
|
2019-07-06 21:40:29 +00:00
|
|
|
// Called on an Entry type that has an action set (either by calling
|
|
|
|
// SetAction on the Entry or by it being returned by FindMatch.
|
|
|
|
// Returns a pointer to type exec.Command and an error (or nil).
|
|
|
|
// The caller needs to remember to set Std in/out/err on the returned
|
|
|
|
// Cmd as is needed by their use case.
|
|
|
|
func (e Entry) Command(path string) (*exec.Cmd, error) {
|
2019-07-03 19:30:49 +00:00
|
|
|
key, ok := e["action"]
|
2019-07-03 18:21:39 +00:00
|
|
|
if !ok {
|
2019-07-06 21:40:29 +00:00
|
|
|
return nil, fmt.Errorf("No action has been set for this entry, use SetAction to set the action")
|
2019-07-03 18:21:39 +00:00
|
|
|
}
|
2019-07-03 19:30:49 +00:00
|
|
|
command, ok := e[key]
|
|
|
|
if !ok {
|
2019-07-06 21:40:29 +00:00
|
|
|
return nil, fmt.Errorf("Entry does not have the key %q", key)
|
2019-07-03 19:30:49 +00:00
|
|
|
}
|
2019-06-29 20:37:56 +00:00
|
|
|
|
2019-07-03 19:30:49 +00:00
|
|
|
matchFields := strings.Fields(command)
|
2019-07-03 18:21:39 +00:00
|
|
|
for index, field := range matchFields {
|
|
|
|
if field == "%s" || field == "'%s'" {
|
|
|
|
matchFields[index] = path
|
|
|
|
}
|
2019-06-29 20:37:56 +00:00
|
|
|
}
|
|
|
|
|
2019-07-03 18:21:39 +00:00
|
|
|
c := exec.Command(matchFields[0], matchFields[1:]...)
|
2019-07-06 21:40:29 +00:00
|
|
|
return c, nil
|
2019-06-29 05:51:20 +00:00
|
|
|
}
|
|
|
|
|
2019-07-03 21:57:32 +00:00
|
|
|
// Sets the action (key) for the entry calling it. This action
|
|
|
|
// is used when calling execute. The action is generally set by the
|
|
|
|
// Mailcap receiver FindMatch and SetAction is provided in order
|
|
|
|
// to easily manipulate an Entry found in the db directly. This
|
|
|
|
// is not recommended, but is provided as a lower level way of
|
|
|
|
// using the lib.
|
2019-07-03 19:30:49 +00:00
|
|
|
func (e Entry) SetAction(action string) error {
|
|
|
|
if _, ok := e[action]; !ok {
|
|
|
|
return fmt.Errorf("This entry does not have the action %q available", action)
|
|
|
|
}
|
|
|
|
e["action"] = action
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-06 21:40:29 +00:00
|
|
|
// Retrieve the command string for a particular action. The
|
|
|
|
// string will not have a path inserted into it and will have
|
2019-07-04 23:10:28 +00:00
|
|
|
// %s to represent the place that the path will go.
|
2019-07-06 21:40:29 +00:00
|
|
|
// Returns a string and error (or nil).
|
|
|
|
func (e Entry) CommandString(action string) (string, error) {
|
2019-07-04 23:10:28 +00:00
|
|
|
if v, ok := e[action]; ok {
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
return "", fmt.Errorf("This entry does not have the action %q available", action)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the available actions/attributes for an entry as
|
|
|
|
// a string slice
|
|
|
|
func (e Entry) Actions() []string {
|
|
|
|
o := make([]string, len(e))
|
|
|
|
i := 0
|
|
|
|
for k, _ := range e {
|
|
|
|
o[i] = k
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return o
|
|
|
|
}
|
|
|
|
|
2019-07-03 18:21:39 +00:00
|
|
|
// Look up all of the Entry types available for
|
|
|
|
// a given mime and key. Returns an Entry slice (Fields)
|
2019-07-06 21:40:29 +00:00
|
|
|
func (m *Mailcap) lookup(mime, key string) []Entry {
|
|
|
|
f := make([]Entry, 0, 5)
|
|
|
|
if val, ok := m.DB[mime]; ok {
|
2019-06-29 05:51:20 +00:00
|
|
|
f = append(f, val...)
|
|
|
|
}
|
|
|
|
splitMime := strings.SplitN(mime,"/",2)
|
|
|
|
catchAllMime := splitMime[0] + "/*"
|
2019-07-06 21:40:29 +00:00
|
|
|
if val, ok := m.DB[catchAllMime]; ok && mime != catchAllMime {
|
2019-06-29 05:51:20 +00:00
|
|
|
f = append(f, val...)
|
|
|
|
}
|
2019-07-06 21:40:29 +00:00
|
|
|
output := make([]Entry, 0, len(f))
|
2019-06-29 05:51:20 +00:00
|
|
|
for _, v := range f {
|
|
|
|
if _, ok := v[key]; ok {
|
|
|
|
output = append(output, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.SliceStable(
|
|
|
|
output,
|
|
|
|
func(i, j int) bool {
|
|
|
|
return output[i]["lineno"] < output[j]["lineno"]
|
|
|
|
})
|
|
|
|
|
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
2019-06-29 20:37:56 +00:00
|
|
|
// Top level private initialization method
|
|
|
|
// Creates the mailcap db and loads it into the
|
|
|
|
// Caps paramater of the Mailcap struct in question
|
2019-06-28 04:49:52 +00:00
|
|
|
func (m *Mailcap) getCaps() {
|
2019-06-28 04:13:55 +00:00
|
|
|
lnNum := 0
|
2019-07-06 21:40:29 +00:00
|
|
|
moreCaps := make(map[string][]Entry)
|
2019-06-28 04:49:52 +00:00
|
|
|
for _, mailcapFile := range getMailcapFileList() {
|
2019-06-28 04:13:55 +00:00
|
|
|
file, err := os.Open(mailcapFile)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2019-06-28 04:49:52 +00:00
|
|
|
moreCaps, lnNum = readMailcapFile(file, lnNum)
|
2019-06-28 04:13:55 +00:00
|
|
|
for k, v := range moreCaps {
|
2019-07-06 21:40:29 +00:00
|
|
|
if _, ok := m.DB[k]; ok {
|
2019-06-28 04:49:52 +00:00
|
|
|
for _, item := range v {
|
2019-07-06 21:40:29 +00:00
|
|
|
m.DB[k] = append(m.DB[k], item)
|
2019-06-28 04:49:52 +00:00
|
|
|
}
|
2019-06-28 04:13:55 +00:00
|
|
|
} else {
|
2019-07-06 21:40:29 +00:00
|
|
|
m.DB[k] = v
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
}
|
2019-06-28 04:49:52 +00:00
|
|
|
file.Close()
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Retrieve a slice of strings with all mailcap files
|
|
|
|
// found on the system.
|
|
|
|
func getMailcapFileList() (mCapSlice []string) {
|
|
|
|
var home string = "."
|
2019-06-28 04:49:52 +00:00
|
|
|
if val, ok := os.LookupEnv("MAILCAPS"); ok {
|
2019-06-28 04:13:55 +00:00
|
|
|
mCapSlice = strings.Split(val, string(os.PathListSeparator))
|
|
|
|
} else {
|
2019-06-28 04:49:52 +00:00
|
|
|
if val, ok := os.LookupEnv("HOME"); ok {
|
2019-06-28 04:13:55 +00:00
|
|
|
home = val
|
|
|
|
}
|
|
|
|
mCapSlice = []string{
|
|
|
|
home + "/.mailcap",
|
|
|
|
"/etc/mailcap",
|
|
|
|
"/usr/etc/mailcap",
|
2019-06-28 04:49:52 +00:00
|
|
|
"/usr/local/etc/mailcap",
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
}
|
2019-06-28 04:49:52 +00:00
|
|
|
return
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
|
2019-06-29 20:37:56 +00:00
|
|
|
// Reads an individual mailcap file and returns a Cap
|
|
|
|
// and a line number
|
2019-07-06 21:40:29 +00:00
|
|
|
func readMailcapFile(f *os.File,ln int) (map[string][]Entry, int) {
|
|
|
|
caps := make(map[string][]Entry)
|
2019-06-28 04:13:55 +00:00
|
|
|
reader := bufio.NewReader(f)
|
|
|
|
for {
|
|
|
|
l, e := reader.ReadString('\n')
|
2019-06-28 05:21:27 +00:00
|
|
|
if e != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if strings.TrimSpace(l) == "" || l[0] == '#' {
|
2019-06-28 04:13:55 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle continuations for long lines
|
|
|
|
nxtLn := l
|
|
|
|
for ;nxtLn[len(nxtLn)-3:] == "\\\n"; {
|
2019-06-28 04:49:52 +00:00
|
|
|
var er error
|
2019-06-28 04:13:55 +00:00
|
|
|
nxtLn, er = reader.ReadString('\n')
|
|
|
|
if er != nil || strings.TrimSpace(nxtLn) == "" {
|
|
|
|
nxtLn = "\n"
|
|
|
|
}
|
|
|
|
l = l[:len(l)-2] + nxtLn
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the line
|
|
|
|
key, fields, err := parseLine(l)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if ln >= 0 {
|
2019-06-29 05:51:20 +00:00
|
|
|
fields["lineno"] = strconv.Itoa(ln)
|
2019-06-28 04:13:55 +00:00
|
|
|
ln += 1
|
|
|
|
}
|
|
|
|
types := strings.Split(key, "/")
|
2019-06-28 04:49:52 +00:00
|
|
|
for i, t := range types {
|
|
|
|
types[i] = strings.TrimSpace(t)
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
key = strings.Join(types, "/")
|
|
|
|
key = strings.ToLower(key)
|
2019-06-28 04:49:52 +00:00
|
|
|
if _, ok := caps[key]; ok {
|
|
|
|
caps[key] = append(caps[key], fields)
|
2019-06-28 04:13:55 +00:00
|
|
|
} else {
|
2019-07-06 21:40:29 +00:00
|
|
|
caps[key] = make([]Entry,0,10)
|
2019-06-28 04:49:52 +00:00
|
|
|
caps[key] = append(caps[key], fields)
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-28 05:21:27 +00:00
|
|
|
return caps, ln
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
|
2019-06-29 20:37:56 +00:00
|
|
|
// Parses an individual mailcap line, returns the key
|
|
|
|
// as a string, all of the fields, and an error or nil.
|
2019-06-28 04:49:52 +00:00
|
|
|
func parseLine(ln string) (string, Entry, error) {
|
2019-06-28 05:21:27 +00:00
|
|
|
outputFields := make(Entry)
|
2019-06-28 04:13:55 +00:00
|
|
|
i := 0
|
|
|
|
n := len(ln)
|
|
|
|
fields := make([]string, 0, 10)
|
2019-06-28 04:49:52 +00:00
|
|
|
var field string
|
2019-06-28 04:13:55 +00:00
|
|
|
for ;i < n; {
|
|
|
|
field, i = parseField(ln, i, n)
|
2019-06-28 04:49:52 +00:00
|
|
|
fields = append(fields, field)
|
2019-06-28 04:13:55 +00:00
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
if len(fields) < 2 {
|
2019-07-06 21:40:29 +00:00
|
|
|
return "", nil,fmt.Errorf("Not enough fields present in line")
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
key, view := fields[0], fields[1]
|
|
|
|
outputFields["view"] = view
|
|
|
|
rest := make([]string,0,0)
|
|
|
|
if len(fields) > 2 {
|
|
|
|
rest = fields[2:]
|
|
|
|
}
|
2019-06-28 04:49:52 +00:00
|
|
|
for _, f := range rest {
|
2019-06-28 04:13:55 +00:00
|
|
|
var fkey, fvalue string
|
2019-06-28 04:49:52 +00:00
|
|
|
i = strings.Index(f, "=")
|
2019-06-28 04:13:55 +00:00
|
|
|
if i < 0 {
|
|
|
|
fkey = f
|
|
|
|
fvalue = ""
|
|
|
|
} else {
|
|
|
|
fkey = strings.TrimSpace(f[:i])
|
|
|
|
fvalue = strings.TrimSpace(f[i+1:])
|
|
|
|
}
|
|
|
|
if _, ok := outputFields[fkey]; !ok {
|
|
|
|
// If the key doesnt exist in the map, add it
|
|
|
|
outputFields[fkey] = fvalue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return key, outputFields, nil
|
|
|
|
}
|
|
|
|
|
2019-07-03 18:21:39 +00:00
|
|
|
// Gets one key-value pair from mailcap entry
|
2019-06-28 04:13:55 +00:00
|
|
|
func parseField(ln string, i, n int) (string, int) {
|
|
|
|
start := i
|
|
|
|
for ;i < n; {
|
|
|
|
c := ln[i]
|
|
|
|
if c == ';' {
|
|
|
|
break
|
|
|
|
} else if c == '\\' {
|
|
|
|
i += 2
|
|
|
|
} else {
|
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
}
|
2019-06-28 04:49:52 +00:00
|
|
|
return strings.TrimSpace(ln[start:i]), i
|
2019-06-28 04:13:55 +00:00
|
|
|
}
|
|
|
|
|