// 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. package mailcap import ( "os" "os/exec" "strings" "strconv" "fmt" "bufio" "sort" ) // 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). type Entry map[string]string // 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. type Mailcap struct { DB map[string][]Entry } // Creates and initializes the mailcap struct/db // and returns a pointer to Mailcap struct func NewMailcap() *Mailcap { mc := Mailcap{make(map[string][]Entry)} mc.getCaps() return &mc } // 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 { return v, nil } return nil, fmt.Errorf("Cannot find %s\n", mime) } // 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". // Also takes a bool, needsTerm, that can be set to true if the // command must be executable in a terminal only environment. // // Returns and Entry and an error func (m *Mailcap) FindMatch(mime, key string, needsTerm bool) (Entry, error) { entries := m.lookup(mime, key) for _, v := range entries { exitCode := 0; if _, ok := v["needsterminal"]; needsTerm && !ok { continue } if t, ok := v["test"]; ok { cmdArgs := strings.Split(t, " ") if len(cmdArgs) < 2 { continue } 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 } v["action"] = key return v, nil } return nil, fmt.Errorf("Unable to find key %q in entries for %s", key, mime) } // 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) { key, ok := e["action"] if !ok { return nil, fmt.Errorf("No action has been set for this entry, use SetAction to set the action") } command, ok := e[key] if !ok { return nil, fmt.Errorf("Entry does not have the key %q", key) } matchFields := strings.Fields(command) for index, field := range matchFields { if field == "%s" || field == "'%s'" { matchFields[index] = path } } c := exec.Command(matchFields[0], matchFields[1:]...) return c, nil } // 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. 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 } // Retrieve the command string for a particular action. The // string will not have a path inserted into it and will have // %s to represent the place that the path will go. // Returns a string and error (or nil). func (e Entry) CommandString(action string) (string, error) { 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 } // Look up all of the Entry types available for // a given mime and key. Returns an Entry slice (Fields) func (m *Mailcap) lookup(mime, key string) []Entry { f := make([]Entry, 0, 5) if val, ok := m.DB[mime]; ok { f = append(f, val...) } splitMime := strings.SplitN(mime,"/",2) catchAllMime := splitMime[0] + "/*" if val, ok := m.DB[catchAllMime]; ok && mime != catchAllMime { f = append(f, val...) } output := make([]Entry, 0, len(f)) 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 } // Top level private initialization method // Creates the mailcap db and loads it into the // Caps paramater of the Mailcap struct in question func (m *Mailcap) getCaps() { lnNum := 0 moreCaps := make(map[string][]Entry) for _, mailcapFile := range getMailcapFileList() { file, err := os.Open(mailcapFile) if err != nil { continue } moreCaps, lnNum = readMailcapFile(file, lnNum) for k, v := range moreCaps { if _, ok := m.DB[k]; ok { for _, item := range v { m.DB[k] = append(m.DB[k], item) } } else { m.DB[k] = v } } file.Close() } } // Retrieve a slice of strings with all mailcap files // found on the system. func getMailcapFileList() (mCapSlice []string) { var home string = "." if val, ok := os.LookupEnv("MAILCAPS"); ok { mCapSlice = strings.Split(val, string(os.PathListSeparator)) } else { if val, ok := os.LookupEnv("HOME"); ok { home = val } mCapSlice = []string{ home + "/.mailcap", "/etc/mailcap", "/usr/etc/mailcap", "/usr/local/etc/mailcap", } } return } // Reads an individual mailcap file and returns a Cap // and a line number func readMailcapFile(f *os.File,ln int) (map[string][]Entry, int) { caps := make(map[string][]Entry) reader := bufio.NewReader(f) for { l, e := reader.ReadString('\n') if e != nil { break } if strings.TrimSpace(l) == "" || l[0] == '#' { continue } // Handle continuations for long lines nxtLn := l for ;nxtLn[len(nxtLn)-3:] == "\\\n"; { var er error 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 { fields["lineno"] = strconv.Itoa(ln) ln += 1 } types := strings.Split(key, "/") for i, t := range types { types[i] = strings.TrimSpace(t) } key = strings.Join(types, "/") key = strings.ToLower(key) if _, ok := caps[key]; ok { caps[key] = append(caps[key], fields) } else { caps[key] = make([]Entry,0,10) caps[key] = append(caps[key], fields) } } return caps, ln } // Parses an individual mailcap line, returns the key // as a string, all of the fields, and an error or nil. func parseLine(ln string) (string, Entry, error) { outputFields := make(Entry) i := 0 n := len(ln) fields := make([]string, 0, 10) var field string for ;i < n; { field, i = parseField(ln, i, n) fields = append(fields, field) i += 1 } if len(fields) < 2 { return "", nil,fmt.Errorf("Not enough fields present in line") } key, view := fields[0], fields[1] outputFields["view"] = view rest := make([]string,0,0) if len(fields) > 2 { rest = fields[2:] } for _, f := range rest { var fkey, fvalue string i = strings.Index(f, "=") 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 } // Gets one key-value pair from mailcap entry 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 } } return strings.TrimSpace(ln[start:i]), i }