summaryrefslogtreecommitdiff
path: root/melib/src/email/list_management.rs
blob: 3ecbb202207d626098f699e6cf34c821843bda14 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/*
 * meli
 *
 * Copyright 2017-2019 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 <http://www.gnu.org/licenses/>.
 */

/*! Parsing of rfc2369/rfc2919 `List-*` headers */
use super::parser;
use super::Envelope;
use smallvec::SmallVec;
use std::convert::From;

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ListAction<'a> {
    Url(&'a [u8]),
    Email(&'a [u8]),
    ///`List-Post` field may contain the special value "NO".
    No,
}

impl<'a> From<&'a [u8]> for ListAction<'a> {
    fn from(value: &'a [u8]) -> Self {
        if value.starts_with(b"mailto:") {
            /* if branch looks if value looks like a mailto url but doesn't validate it.
             * parser::mailto() will handle this if user tries to unsubscribe.
             */
            ListAction::Email(value)
        } else if value.starts_with(b"NO") {
            ListAction::No
        } else {
            /* Otherwise treat it as url. There's no foolproof way to check if this is valid, so
             * postpone it until we try an HTTP request.
             */
            ListAction::Url(value)
        }
    }
}

impl<'a> ListAction<'a> {
    pub fn parse_options_list(input: &'a [u8]) -> Option<SmallVec<[ListAction<'a>; 4]>> {
        parser::mailing_lists::rfc_2369_list_headers_action_list(input)
            .map(|(_, mut vec)| {
                /* Prefer email options first, since this _is_ a mail client after all and it's
                 * more automated */
                vec.sort_unstable_by(|a, b| {
                    match (a.starts_with(b"mailto:"), b.starts_with(b"mailto:")) {
                        (true, false) => std::cmp::Ordering::Less,
                        (false, true) => std::cmp::Ordering::Greater,
                        _ => std::cmp::Ordering::Equal,
                    }
                });

                vec.into_iter()
                    .map(ListAction::from)
                    .collect::<SmallVec<[ListAction<'a>; 4]>>()
            })
            .ok()
    }
}

#[derive(Default, Debug)]
pub struct ListActions<'a> {
    pub id: Option<&'a str>,
    pub archive: Option<&'a str>,
    pub post: Option<SmallVec<[ListAction<'a>; 4]>>,
    pub unsubscribe: Option<SmallVec<[ListAction<'a>; 4]>>,
}

pub fn list_id_header(envelope: &'_ Envelope) -> Option<&'_ str> {
    envelope
        .other_headers()
        .get("List-ID")
        .or_else(|| envelope.other_headers().get("List-Id"))
        .map(String::as_str)
}

pub fn list_id(header: Option<&'_ str>) -> Option<&'_ str> {
    /* rfc2919 https://tools.ietf.org/html/rfc2919 */
    /* list-id-header = "List-ID:" [phrase] "<" list-id ">" CRLF */
    header.and_then(|v| {
        if let Some(l) = v.rfind('<') {
            if let Some(r) = v.rfind('>') {
                if l < r {
                    return Some(&v[l + 1..r]);
                }
            }
        }
        None
    })
}

impl<'a> ListActions<'a> {
    pub fn detect(envelope: &'a Envelope) -> Option<ListActions<'a>> {
        let mut ret = ListActions::default();

        ret.id = list_id_header(envelope);

        if let Some(archive) = envelope.other_headers().get("List-Archive") {
            if archive.starts_with('<') {
                if let Some(pos) = archive.find('>') {
                    ret.archive = Some(&archive[1..pos]);
                } else {
                    ret.archive = Some(archive);
                }
            } else {
                ret.archive = Some(archive);
            }
        }

        if let Some(post) = envelope.other_headers().get("List-Post") {
            ret.post = ListAction::parse_options_list(post.as_bytes());
            if let Some(ref l) = ret.post {
                if l.starts_with(&[ListAction::No]) {
                    ret.post = None;
                }
            }
        }

        if let Some(unsubscribe) = envelope.other_headers().get("List-Unsubscribe") {
            ret.unsubscribe = ListAction::parse_options_list(unsubscribe.as_bytes());
        }

        if ret.id.is_none()
            && ret.archive.is_none()
            && ret.post.is_none()
            && ret.unsubscribe.is_none()
        {
            None
        } else {
            Some(ret)
        }
    }
}