@@ 14,6 14,62 @@
// You should have received a copy of the GNU General Public License
// along with mailcap. If not, see <http://www.gnu.org/licenses/>.
+//! # mailcap
+//!
+//! `mailcap` is a parsing library for mailcap files.
+//!
+//! Mailcap files are a format documented in [RFC
+//! 1524](https://www.rfc-editor.org/rfc/rfc1524.html), "A User Agent Configuration
+//! Mechanism For Multimedia Mail Format Information." They allow the handling of
+//! MIME types by software aware of those types. For example, a mailcap line of
+//! `text/html; qutebrowser '%s'; test=test -n "$DISPLAY"` would instruct the
+//! software to open any HTML file with qutebrowser if you are running a graphical
+//! session, with the file replacing the `'%s'`.
+//!
+//! `mailcap` is a parsing library that looks at either a present `$MAILCAPS` env
+//! variable or cycles through the four paths where a mailcap file would be found in
+//! ascending order of importance: `/usr/local/etc/mailcap`, `/usr/etc/mailcap`,
+//! `/etc/mailcap`, and `$HOME/.mailcap`. It builds the mailcap from all available
+//! files, with duplicate entries being squashed with newer lines, allowing
+//! `$HOME/.mailcap` to be the final decider.
+//!
+//! The entries that make up the mailcap include only those that are relevant i.e.
+//! those that have passed the `test` field (if present). With the above `text/html`
+//! example, that test would fail if run through SSH, and unless another existing
+//! `text/html` entry (or `text/*`) exists that doesn't require a display server, no
+//! entry would exist for that mime type.
+//!
+//! # Usage
+//!
+//! ```rust
+//! use mailcap::{Mailcap, MailcapError};
+//!
+//! fn main() -> Result<(), MailcapError> {
+//! let cap = Mailcap::new()?;
+//! if let Some(i) = cap.get("text/html") {
+//! let command = i.viewer("/var/www/index.html");
+//! assert_eq!(command, "qutebrowser '/var/www/index.html'");
+//! }
+//! Ok(())
+//! }
+//! ```
+//!
+//! Wildcard fallbacks are also supported.
+//!
+//! ```rust
+//! use mailcap::{Mailcap, MailcapError};
+//!
+//! fn main() -> Result<(), MailcapError> {
+//! let cap = Mailcap::new()?;
+//! if let Some(i) = cap.get("video/avi") {
+//! // if no video/avi MIME entry available
+//! let mime_type = i.mime();
+//! assert_eq!(mime_type, "video/*");
+//! }
+//! Ok(())
+//! }
+//! ```
+
use libc::system;
use std::collections::HashMap;
use std::ffi::CString;
@@ 22,19 78,27 @@ use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::{env, fmt};
+/// The error type for `mailcap`.
#[derive(Debug, PartialEq)]
pub enum MailcapError {
+ /// The mailcap line was unable to be parsed.
LineParseError,
+ /// The mailcap file was unable to be parsed.
FileParseError,
+ /// There are no valid mailcap files to parse.
NoValidFiles,
}
+/// Meta representation of all available mailcap files and their combined lines.
#[derive(Default, Debug, PartialEq, Clone)]
pub struct Mailcap {
files: Vec<PathBuf>,
data: HashMap<String, Entry>,
}
+/// Parsed mailcap line. Each mailcap entry consists of a number of fields, separated by
+/// semi-colons. The first two fields are required, and must occur in the specified order. The
+/// remaining fields are optional, and may appear in any order.
#[derive(Default, Debug, PartialEq, Clone)]
pub struct Entry {
mime_type: String,
@@ 44,12 108,11 @@ pub struct Entry {
edit: Option<String>,
print: Option<String>,
test: Option<String>,
- note: Option<String>,
description: Option<String>,
name_template: Option<String>,
needs_terminal: bool,
copious_output: bool,
- textual_new_lines: bool, // either 1 or 0 in file
+ textual_new_lines: bool,
}
impl fmt::Display for MailcapError {
@@ 63,6 126,30 @@ impl fmt::Display for MailcapError {
}
impl Mailcap {
+ /// Returns a combined mailcap from all available default files or a $MAILCAPS env.
+ /// The default list (in ascending order of importance) includes:
+ ///
+ /// - `/usr/local/etc/mailcap`
+ /// - `/usr/etc/mailcap`
+ /// - `/etc/mailcap`
+ /// - `$HOME/.mailcap`
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # use mailcap::{Mailcap, MailcapError};
+ /// # fn main() -> Result<(), MailcapError> {
+ /// let cap = Mailcap::new()?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// If there are no available mailcap files in the default locations or no $MAILCAPS env has
+ /// been set, or if the files or empty or contain no valid mailcap lines, `MailcapError` will
+ /// be returned. The implementation is loose: as long as one file exists with at least one
+ /// valid mailcap line, the `Result` will be `Ok`.
pub fn new() -> Result<Mailcap, MailcapError> {
let mut files = Self::list_potential_files();
Self::check_files_exist(&mut files)?;
@@ 87,6 174,22 @@ impl Mailcap {
Ok(Mailcap { files, data })
}
+ /// Given a specific mime-type value, will lookup if there is an existing mailcap entry for
+ /// that type.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # use mailcap::{Mailcap, MailcapError};
+ /// # fn main() -> Result<(), MailcapError> {
+ /// let cap = Mailcap::new()?;
+ /// if let Some(i) = cap.get("text/html") {
+ /// let command = i.viewer("/var/www/index.html");
+ /// assert_eq!(command, "qutebrowser '/var/www/index.html'");
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
pub fn get(&self, key: &str) -> Option<&Entry> {
match self.data.get(key) {
Some(v) => Some(v),
@@ 185,7 288,6 @@ impl Mailcap {
impl Entry {
fn from(line: &Vec<String>) -> Option<Entry> {
let mut entry = Entry::default();
- // TODO: validate mime_type against database
entry.mime_type = line[0].to_owned();
entry.viewer = line[1].to_owned();
@@ 196,7 298,6 @@ impl Entry {
Some(("edit", v)) => entry.edit = Some(v[1..].to_string()),
Some(("print", v)) => entry.print = Some(v[1..].to_string()),
Some(("test", v)) => entry.test = Some(v[1..].to_string()),
- Some(("note", v)) => entry.note = Some(v[1..].to_string()),
Some(("description", v)) => entry.description = Some(v[1..].to_string()),
Some(("nametemplate", v)) => entry.name_template = Some(v[1..].to_string()),
Some(("needsterminal", _)) => entry.needs_terminal = true,
@@ 215,54 316,111 @@ impl Entry {
}
}
+ /// The `mime_type`, which indicates the type of data this mailcap entry describes how to
+ /// handle. It is to be matched against the type/subtype specification in the "Content-Type"
+ /// header field of an Internet mail message. If the subtype is specified as "*", it is
+ /// intended to match all subtypes of the named `mime_type`.
pub fn mime(&self) -> &String {
&self.mime_type
}
+ /// The second field, `viewer`, is a specification of how the message or body part can be
+ /// viewed at the local site. Although the syntax of this field is fully specified, the
+ /// semantics of program execution are necessarily somewhat operating system dependent. UNIX
+ /// semantics are given in Appendix A of RFC 1524.
pub fn viewer(&self, filename: &str) -> String {
self.viewer.replace("%s", filename)
}
+ /// The `compose` field may be used to specify a program that can be used to compose a new body
+ /// or body part in the given format. Its intended use is to support mail composing agents that
+ /// support the composition of multiple types of mail using external composing agents. As with
+ /// `viewer`, the semantics of program execution are operating system dependent, with UNIX
+ /// semantics specified in Appendix A of RFC 1524. The result of the composing program may be
+ /// data that is not yet suitable for mail transport -- that is, a Content-Transfer-Encoding
+ /// may need to be applied to the data.
pub fn compose(&self) -> &Option<String> {
&self.compose
}
+ /// The `compose_typed` field is similar to the `compose` field, but is to be used when the
+ /// composing program needs to specify the Content-type header field to be applied to the
+ /// composed data. The `compose` field is simpler, and is preferred for use with existing
+ /// (non-mail-oriented) programs for composing data in a given format. The `compose_typed` field
+ /// is necessary when the Content-type information must include auxilliary parameters, and the
+ /// composition program must then know enough about mail formats to produce output that
+ /// includes the mail type information.
pub fn compose_typed(&self) -> &Option<String> {
&self.compose_typed
}
+ /// The `edit` field may be used to specify a program that can be used to edit a body or body
+ /// part in the given format. In many cases, it may be identical in content to the `compose`
+ /// field, and shares the operating-system dependent semantics for program execution.
pub fn edit(&self) -> &Option<String> {
&self.edit
}
+ /// The `print` field may be used to specify a program that can be used to print a message or
+ /// body part in the given format. As with `viewer`, the semantics of program execution are
+ /// operating system dependent, with UNIX semantics specified in Appendix A of RFC 1524.
pub fn print(&self) -> &Option<String> {
&self.print
}
+ /// The `test` field may be used to test some external condition (e.g., the machine
+ /// architecture, or the window system in use) to determine whether or not the mailcap line
+ /// applies. It specifies a program to be run to test some condition. The semantics of
+ /// execution and of the value returned by the test program are operating system dependent,
+ /// with UNIX semantics specified in Appendix A of RFC 1524. If the test fails, a subsequent
+ /// mailcap entry should be sought. Multiple test fields are not permitted -- since a test can
+ /// call a program, it can already be arbitrarily complex.
pub fn test(&self) -> &Option<String> {
&self.test
}
- pub fn note(&self) -> &Option<String> {
- &self.note
- }
-
+ /// The `description` field simply provides a textual description, optionally quoted, that
+ /// describes the type of data, to be used optionally by mail readers that wish to describe the
+ /// data before offering to display it.
pub fn description(&self) -> &Option<String> {
&self.description
}
+ /// The `name_template` field gives a file name format, in which %s will be replaced by a short
+ /// unique string to give the name of the temporary file to be passed to the viewing command.
+ /// This is only expected to be relevant in environments where filename extensions are
+ /// meaningful, e.g., one coulld specify that a GIF file being passed to a gif viewer should
+ /// have a name eding in ".gif" by using "nametemplate=%s.gif".
pub fn name_template(&self) -> &Option<String> {
&self.name_template
}
+ /// The `needs_terminal` field indicates that the `viewer` must be run on an interactive
+ /// terminal. This is needed to inform window-oriented user agents that an interactive
+ /// terminal is needed. (The decision is not left exclusively to `viewer` because in
+ /// some circumstances it may not be possible for such programs to tell whether or not they are
+ /// on interactive terminals). The `needs_terminal` command should be assumed to apply to the
+ /// compose and edit commands, too, if they exist. Note that this is NOT a test -- it is a
+ /// requirement for the environment in which the program will be executed, and should typically
+ /// cause the creation of a terminal window when not executed on either a real terminal or a
+ /// terminal window.
pub fn needs_terminal(&self) -> &bool {
&self.needs_terminal
}
+ /// The `copious_output` field indicates that the output from `viewer` will be an
+ /// extended stream of output, and is to be interpreted as advice to the UA (User Agent
+ /// mail-reading program) that the output should be either paged or made scrollable. Note that
+ /// it is probably a mistake if `needs_terminal` and `copious_output` are both specified.
pub fn copious_output(&self) -> &bool {
&self.copious_output
}
+ /// The `textual_new_lines` field, if set to any non-zero value, indicates that this type of data
+ /// is line-oriented and that, if encoded in base64, all newlines should be converted to
+ /// canonical form (CRLF) before encoding, and will be in that form after decoding. In general,
+ /// this field is needed only if there is line-oriented data of some type other than text/* or
+ /// non-line-oriented data that is a subtype of text.
pub fn textual_new_lines(&self) -> &bool {
&self.textual_new_lines
}
@@ 310,7 468,6 @@ mod tests {
edit: None,
print: None,
test: Some("test -n \"$DISPLAY\"".to_string()),
- note: None,
description: None,
name_template: Some("%s.html".to_string()),
needs_terminal: true,
@@ 328,7 485,6 @@ mod tests {
edit: None,
print: None,
test: Some("test -n \"$DISPLAY\"".to_string()),
- note: None,
description: None,
name_template: Some("%s.html".to_string()),
needs_terminal: true,
@@ 428,7 584,6 @@ mod tests {
edit: None,
print: None,
test: Some("test -n \"$DISPLAY\"".to_string()),
- note: None,
description: None,
name_template: Some("%s.html".to_string()),
needs_terminal: true,