/*
* meli - mailbox module.
*
* Copyright 2017 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see .
*/
/*!
* https://wiki2.dovecot.org/MailboxFormat/mbox
*/
use crate::backends::*;
use crate::conf::AccountSettings;
use crate::email::parser::BytesExt;
use crate::email::*;
use crate::error::{MeliError, Result};
use crate::get_path_hash;
use crate::shellexpand::ShellExpandTrait;
use memmap::{Mmap, Protection};
use nom::bytes::complete::tag;
use nom::character::complete::digit1;
use nom::combinator::map_res;
use nom::{self, error::ErrorKind, IResult};
extern crate notify;
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::collections::hash_map::{DefaultHasher, HashMap};
use std::fs::File;
use std::hash::Hasher;
use std::io::BufReader;
use std::io::Read;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex, RwLock};
type Offset = usize;
type Length = usize;
const F_OFD_SETLKW: libc::c_int = 38;
// Open file description locking
// # man fcntl
fn get_rw_lock_blocking(f: &File) {
let fd: libc::c_int = f.as_raw_fd();
let mut flock: libc::flock = libc::flock {
l_type: libc::F_WRLCK as libc::c_short,
l_whence: libc::SEEK_SET as libc::c_short,
l_start: 0,
l_len: 0, /* "Specifying 0 for l_len has the special meaning: lock all bytes starting at the location
specified by l_whence and l_start through to the end of file, no matter how large the file grows." */
l_pid: 0, /* "By contrast with traditional record locks, the l_pid field of that structure must be set to zero when using the commands described below." */
};
let ptr: *mut libc::flock = &mut flock;
let ret_val = unsafe { libc::fcntl(fd, F_OFD_SETLKW, ptr as *mut libc::c_void) };
debug!(&ret_val);
assert!(-1 != ret_val);
}
#[derive(Debug)]
struct MboxMailbox {
hash: MailboxHash,
name: String,
path: PathBuf,
fs_path: PathBuf,
content: Vec,
children: Vec,
parent: Option,
usage: Arc>,
is_subscribed: bool,
permissions: MailboxPermissions,
pub total: Arc>,
pub unseen: Arc>,
index: Arc>>,
}
impl BackendMailbox for MboxMailbox {
fn hash(&self) -> MailboxHash {
self.hash
}
fn name(&self) -> &str {
self.name.as_str()
}
fn path(&self) -> &str {
/* We know it's valid UTF-8 because we supplied it */
self.path.to_str().unwrap()
}
fn change_name(&mut self, s: &str) {
self.name = s.to_string();
}
fn clone(&self) -> Mailbox {
Box::new(MboxMailbox {
hash: self.hash,
name: self.name.clone(),
path: self.path.clone(),
fs_path: self.fs_path.clone(),
content: self.content.clone(),
children: self.children.clone(),
usage: self.usage.clone(),
is_subscribed: self.is_subscribed,
parent: self.parent,
permissions: self.permissions,
unseen: self.unseen.clone(),
total: self.total.clone(),
index: self.index.clone(),
})
}
fn children(&self) -> &[MailboxHash] {
&self.children
}
fn parent(&self) -> Option {
self.parent
}
fn special_usage(&self) -> SpecialUsageMailbox {
*self.usage.read().unwrap()
}
fn permissions(&self) -> MailboxPermissions {
self.permissions
}
fn is_subscribed(&self) -> bool {
self.is_subscribed
}
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val;
Ok(())
}
fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> {
*self.usage.write()? = new_val;
Ok(())
}
fn count(&self) -> Result<(usize, usize)> {
Ok((*self.unseen.lock()?, *self.total.lock()?))
}
}
/// `BackendOp` implementor for Mbox
#[derive(Debug, Default)]
pub struct MboxOp {
hash: EnvelopeHash,
path: PathBuf,
offset: Offset,
length: Length,
slice: Option,
}
impl MboxOp {
pub fn new(hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
MboxOp {
hash,
path: path.to_path_buf(),
slice: None,
offset,
length,
}
}
}
impl BackendOp for MboxOp {
fn as_bytes(&mut self) -> ResultFuture> {
if self.slice.is_none() {
self.slice = Some(Mmap::open_path(&self.path, Protection::Read)?);
}
/* Unwrap is safe since we use ? above. */
let ret = Ok((unsafe {
&self.slice.as_ref().unwrap().as_slice()[self.offset..self.offset + self.length]
})
.to_vec());
Ok(Box::pin(async move { ret }))
}
fn fetch_flags(&self) -> ResultFuture {
let mut flags = Flag::empty();
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&self.path)?;
get_rw_lock_blocking(&file);
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
let (_, headers) = parser::headers::headers_raw(contents.as_slice())?;
if let Some(start) = headers.find(b"Status:") {
if let Some(end) = headers[start..].find(b"\n") {
let start = start + b"Status:".len();
let status = headers[start..start + end].trim();
if status.contains(&b'F') {
flags.set(Flag::FLAGGED, true);
}
if status.contains(&b'A') {
flags.set(Flag::REPLIED, true);
}
if status.contains(&b'R') {
flags.set(Flag::SEEN, true);
}
if status.contains(&b'D') {
flags.set(Flag::TRASHED, true);
}
if status.contains(&b'T') {
flags.set(Flag::DRAFT, true);
}
}
}
if let Some(start) = headers.find(b"X-Status:") {
let start = start + b"X-Status:".len();
if let Some(end) = headers[start..].find(b"\n") {
let status = headers[start..start + end].trim();
if status.contains(&b'F') {
flags.set(Flag::FLAGGED, true);
}
if status.contains(&b'A') {
flags.set(Flag::REPLIED, true);
}
if status.contains(&b'R') {
flags.set(Flag::SEEN, true);
}
if status.contains(&b'D') {
flags.set(Flag::TRASHED, true);
}
if status.contains(&b'T') {
flags.set(Flag::DRAFT, true);
}
}
}
Ok(Box::pin(async move { Ok(flags) }))
}
}
#[derive(Debug, Clone, Copy)]
pub enum MboxReader {
MboxO,
MboxRd,
MboxCl,
MboxCl2,
}
impl Default for MboxReader {
fn default() -> Self {
Self::MboxCl2
}
}
macro_rules! find_From__line {
($input:expr) => {{
//debug!("find_From__line invocation");
let input = $input;
let mut ptr = 0;
let mut found = None;
while ptr < input.len() {
// Find next From_ candidate line.
const TAG: &'static [u8] = b"\n\nFrom ";
if let Some(end) = input[ptr..].find(TAG) {
// This candidate is a valid From_ if it ends in a new line and the next line is
// a header.
if let Some(line_end) = input[ptr + end + TAG.len()..].find(b"\n") {
if crate::email::parser::headers::header(
&input[ptr + end + TAG.len() + line_end + 1..],
)
.is_ok()
{
found = Some(ptr + end);
break;
} else {
/* Ignore invalid From_ line. */
ptr += end + TAG.len() + line_end;
}
} else {
/* Ignore invalid From_ line. */
ptr += end + TAG.len();
}
} else {
found = Some(input.len());
break;
}
}
found
}};
}
impl MboxReader {
fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
let orig_input = input;
let mut input = input;
match self {
Self::MboxO => {
let next_offset: Option<(usize, usize)> = find_From__line!(input)
.and_then(|end| input.find(b"\n").map(|start| (start + 1, end)));
if let Some((start, len)) = next_offset {
match Envelope::from_bytes(&input[start..len], None) {
Ok(mut env) => {
let mut flags = Flag::empty();
if env.other_headers().contains_key("Status") {
if env.other_headers()["Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
}
if env.other_headers().contains_key("X-Status") {
if env.other_headers()["X-Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["X-Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["X-Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["X-Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
if env.other_headers()["X-Status"].contains('T') {
flags.set(Flag::DRAFT, true);
}
}
env.set_flags(flags);
if len == input.len() {
Ok((&[], env))
} else {
input = &input[len + 2..];
Ok((input, env))
}
}
Err(err) => {
debug!("Could not parse mail {:?}", err);
Err(nom::Err::Error((input, ErrorKind::Tag)))
}
}
} else {
let start: Offset = input.find(b"\n").map(|v| v + 1).unwrap_or(0);
match Envelope::from_bytes(&input[start..], None) {
Ok(mut env) => {
let mut flags = Flag::empty();
if env.other_headers().contains_key("Status") {
if env.other_headers()["Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
}
if env.other_headers().contains_key("X-Status") {
if env.other_headers()["X-Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["X-Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["X-Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["X-Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
if env.other_headers()["X-Status"].contains('T') {
flags.set(Flag::DRAFT, true);
}
}
env.set_flags(flags);
Ok((&[], env))
}
Err(err) => {
debug!("Could not parse mail at {:?}", err);
Err(nom::Err::Error((input, ErrorKind::Tag)))
}
}
}
}
Self::MboxRd => {
let next_offset: Option<(usize, usize)> = find_From__line!(input)
.and_then(|end| input.find(b"\n").map(|start| (start + 1, end)));
if let Some((start, len)) = next_offset {
match Envelope::from_bytes(&input[start..len], None) {
Ok(mut env) => {
let mut flags = Flag::empty();
if env.other_headers().contains_key("Status") {
if env.other_headers()["Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
}
if env.other_headers().contains_key("X-Status") {
if env.other_headers()["X-Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["X-Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["X-Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["X-Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
if env.other_headers()["X-Status"].contains('T') {
flags.set(Flag::DRAFT, true);
}
}
env.set_flags(flags);
if len == input.len() {
Ok((&[], env))
} else {
input = &input[len + 2..];
Ok((input, env))
}
}
Err(err) => {
debug!("Could not parse mail {:?}", err);
Err(nom::Err::Error((input, ErrorKind::Tag)))
}
}
} else {
let start: Offset = input.find(b"\n").map(|v| v + 1).unwrap_or(0);
match Envelope::from_bytes(&input[start..], None) {
Ok(mut env) => {
let mut flags = Flag::empty();
if env.other_headers().contains_key("Status") {
if env.other_headers()["Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
}
if env.other_headers().contains_key("X-Status") {
if env.other_headers()["X-Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["X-Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["X-Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["X-Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
if env.other_headers()["X-Status"].contains('T') {
flags.set(Flag::DRAFT, true);
}
}
env.set_flags(flags);
Ok((&[], env))
}
Err(err) => {
debug!("Could not parse mail {:?}", err);
Err(nom::Err::Error((input, ErrorKind::Tag)))
}
}
}
}
Self::MboxCl | Self::MboxCl2 => {
let start: Offset = input.find(b"\n").map(|v| v + 1).unwrap_or(0);
input = &input[start..];
let headers_end: usize = input.find(b"\n\n").unwrap_or(input.len());
let content_length = if let Some(v) = input[..headers_end].find(b"Content-Length: ")
{
v
} else {
// Is not MboxCl{,2}
return Self::MboxRd.parse(orig_input);
};
let (_input, _) = if let Ok(s) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(
"Content-Length:",
)(&input[content_length..])
{
s
} else {
return Self::MboxRd.parse(orig_input);
};
let (_input, bytes) = if let Ok(s) =
map_res::<&[u8], _, _, (&[u8], nom::error::ErrorKind), _, _, _>(
digit1,
|s: &[u8]| String::from_utf8_lossy(s).parse::(),
)(_input.ltrim())
{
s
} else {
return Self::MboxRd.parse(orig_input);
};
match Envelope::from_bytes(&input[..headers_end + bytes], None) {
Ok(mut env) => {
let mut flags = Flag::empty();
if env.other_headers().contains_key("Status") {
if env.other_headers()["Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
}
if env.other_headers().contains_key("X-Status") {
if env.other_headers()["X-Status"].contains('F') {
flags.set(Flag::FLAGGED, true);
}
if env.other_headers()["X-Status"].contains('A') {
flags.set(Flag::REPLIED, true);
}
if env.other_headers()["X-Status"].contains('R') {
flags.set(Flag::SEEN, true);
}
if env.other_headers()["X-Status"].contains('D') {
flags.set(Flag::TRASHED, true);
}
if env.other_headers()["X-Status"].contains('T') {
flags.set(Flag::DRAFT, true);
}
}
env.set_flags(flags);
if headers_end + 2 + bytes >= input.len() {
Ok((&[], env))
} else {
input = &input[headers_end + 3 + bytes..];
Ok((input, env))
}
}
Err(_err) => Self::MboxRd.parse(orig_input),
}
}
}
}
}
pub fn mbox_parse(
index: Arc>>,
input: &[u8],
file_offset: usize,
reader: Option,
) -> IResult<&[u8], Vec> {
if input.is_empty() {
return Err(nom::Err::Error((input, ErrorKind::Tag)));
}
let mut offset = 0;
let mut index = index.lock().unwrap();
let mut envelopes = Vec::with_capacity(32);
let reader = reader.unwrap_or(MboxReader::MboxCl2);
while !input[offset + file_offset..].is_empty() {
let (next_input, env) = match reader.parse(&input[offset + file_offset..]) {
Ok(v) => v,
Err(e) => {
// Try to recover from this error by finding a new candidate From_ line
if let Some(next_offset) = find_From__line!(&input[offset + file_offset..]) {
offset += next_offset;
if offset != input.len() {
// If we are not at EOF, we will be at this point
// "\n\nFrom ..."
// ↑
// So, skip those two newlines.
offset += 2;
}
} else {
return Err(e);
}
continue;
}
};
let start: Offset = input[offset + file_offset..]
.find(b"\n")
.map(|v| v + 1)
.unwrap_or(0);
let len = input.len() - next_input.len() - offset - file_offset - start;
index.insert(env.hash(), (offset + file_offset + start, len));
offset += len + start;
envelopes.push(env);
}
Ok((&[], envelopes))
}
struct MessageIterator<'a> {
index: Arc>>,
input: &'a [u8],
file_offset: usize,
offset: usize,
reader: Option,
}
impl<'a> Iterator for MessageIterator<'a> {
type Item = Result;
fn next(&mut self) -> Option {
if self.input.is_empty() {
return None;
}
let mut index = self.index.lock().unwrap();
let reader = self.reader.unwrap_or(MboxReader::MboxCl2);
while !self.input[self.offset + self.file_offset..].is_empty() {
let (next_input, env) =
match reader.parse(&self.input[self.offset + self.file_offset..]) {
Ok(v) => v,
Err(e) => {
// Try to recover from this error by finding a new candidate From_ line
if let Some(next_offset) =
find_From__line!(&self.input[self.offset + self.file_offset..])
{
self.offset += next_offset;
if self.offset != self.input.len() {
// If we are not at EOF, we will be at this point
// "\n\nFrom ..."
// ↑
// So, skip those two newlines.
self.offset += 2;
}
} else {
self.input = b"";
return Some(Err(e.into()));
}
continue;
}
};
let start: Offset = self.input[self.offset + self.file_offset..]
.find(b"\n")
.map(|v| v + 1)
.unwrap_or(0);
let len = self.input.len() - next_input.len() - self.offset - self.file_offset - start;
index.insert(env.hash(), (self.offset + self.file_offset + start, len));
self.offset += len + start;
return Some(Ok(env));
}
None
}
}
/// Mbox backend
#[derive(Debug)]
pub struct MboxType {
account_name: String,
path: PathBuf,
mailbox_index: Arc>>,
mailboxes: Arc>>,
prefer_mbox_type: Option,
event_consumer: BackendEventConsumer,
}
impl MailBackend for MboxType {
fn capabilities(&self) -> MailBackendCapabilities {
const CAPABILITIES: MailBackendCapabilities = MailBackendCapabilities {
is_async: false,
is_remote: false,
supports_search: false,
extensions: None,
supports_tags: false,
supports_submission: false,
};
CAPABILITIES
}
fn is_online(&self) -> ResultFuture<()> {
Ok(Box::pin(async { Ok(()) }))
}
fn fetch(
&mut self,
mailbox_hash: MailboxHash,
) -> Result>> + Send + 'static>>> {
struct FetchState {
mailbox_hash: MailboxHash,
mailbox_index: Arc>>,
mailboxes: Arc>>,
prefer_mbox_type: Option,
offset: usize,
file_offset: usize,
contents: Vec,
}
impl FetchState {
async fn fetch(&mut self) -> Result