sieve/runtime/actions/
action_mime.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use std::cmp::Reverse;
8
9use mail_parser::{
10    decoders::html::html_to_text, Encoding, HeaderName, Message, MessagePart, PartType,
11};
12
13use crate::{
14    compiler::{
15        grammar::actions::action_mime::{Enclose, ExtractText, Replace},
16        VariableType,
17    },
18    Context, Event,
19};
20
21use super::action_editheader::RemoveCrLf;
22
23#[cfg(not(test))]
24use mail_builder::headers::message_id::generate_message_id_header;
25
26impl Replace {
27    pub(crate) fn exec(&self, ctx: &mut Context) {
28        // Delete children parts
29        let mut part_ids = ctx.find_nested_parts_ids(false);
30        part_ids.sort_unstable_by_key(|a| Reverse(*a));
31        for part_id in part_ids {
32            ctx.message.parts.remove(part_id as usize);
33        }
34        ctx.has_changes = true;
35
36        // Update part
37        let body = ctx.eval_value(&self.replacement).to_string().into_owned();
38        let body_len = body.len();
39
40        let part = &mut ctx.message.parts[ctx.part as usize];
41
42        ctx.message_size = ctx.message_size + body_len
43            - (if part.offset_body != 0 {
44                (part.offset_end - part.offset_header) as usize
45            } else {
46                part.body.len()
47            });
48        part.body = PartType::Text(body.into());
49        part.encoding = if !self.mime {
50            Encoding::QuotedPrintable
51        } else {
52            Encoding::None
53        };
54        part.offset_body = 0;
55        let prev_headers = std::mem::take(&mut part.headers);
56        let mut add_date = true;
57
58        if ctx.part == 0 {
59            for mut header in prev_headers {
60                let mut size = (header.offset_end - header.offset_field) as usize;
61                match &header.name {
62                    HeaderName::Subject => {
63                        if self.subject.is_some() {
64                            header.name = HeaderName::Other("Original-Subject".into());
65                            header.offset_field = header.offset_start;
66                            size += "Original-".len();
67                        }
68                    }
69                    HeaderName::From => {
70                        if self.from.is_some() {
71                            header.name = HeaderName::Other("Original-From".into());
72                            header.offset_field = header.offset_start;
73                            size += "Original-".len();
74                        }
75                    }
76
77                    HeaderName::To | HeaderName::Cc | HeaderName::Bcc | HeaderName::Received => (),
78                    HeaderName::Date => {
79                        add_date = false;
80                    }
81                    _ => continue,
82                }
83                ctx.message_size += size;
84                part.headers.push(header);
85            }
86
87            // Add From
88            let mut add_from = true;
89            if let Some(from) = self.from.as_ref().map(|f| ctx.eval_value(f)) {
90                if !from.is_empty() {
91                    ctx.insert_header(
92                        0,
93                        HeaderName::Other("From".into()),
94                        from.to_string()
95                            .as_ref()
96                            .remove_crlf(ctx.runtime.max_header_size),
97                        true,
98                    );
99                    add_from = false;
100                }
101            }
102            if add_from {
103                ctx.insert_header(
104                    0,
105                    HeaderName::Other("From".to_string().into()),
106                    ctx.user_from_field(),
107                    true,
108                );
109            }
110
111            // Add Subject
112            if let Some(subject) = self.subject.as_ref().map(|f| ctx.eval_value(f)) {
113                if !subject.is_empty() {
114                    ctx.insert_header(
115                        0,
116                        HeaderName::Other("Subject".into()),
117                        subject
118                            .to_string()
119                            .as_ref()
120                            .remove_crlf(ctx.runtime.max_header_size),
121                        true,
122                    );
123                }
124            }
125
126            // Add Date
127            if add_date {
128                #[cfg(not(test))]
129                let header_value = mail_builder::headers::date::Date::now().to_rfc822();
130                #[cfg(test)]
131                let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string();
132
133                ctx.insert_header(
134                    0,
135                    HeaderName::Other("Date".to_string().into()),
136                    header_value,
137                    true,
138                );
139            }
140
141            // Add Message-ID
142            let mut header_value = Vec::with_capacity(20);
143            #[cfg(not(test))]
144            generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap();
145            #[cfg(test)]
146            header_value.extend_from_slice(b"<auto-generated@message-id>");
147
148            ctx.insert_header(
149                0,
150                HeaderName::Other("Message-ID".to_string().into()),
151                String::from_utf8(header_value).unwrap(),
152                true,
153            );
154        }
155
156        if !self.mime {
157            ctx.insert_header(
158                ctx.part,
159                HeaderName::Other("Content-Type".into()),
160                "text/plain; charset=utf-8".to_string(),
161                true,
162            );
163        }
164    }
165}
166
167impl Enclose {
168    pub(crate) fn exec(&self, ctx: &mut Context) {
169        let body = ctx.eval_value(&self.value).to_string().into_owned();
170        let subject = self
171            .subject
172            .as_ref()
173            .map(|s| {
174                ctx.eval_value(s)
175                    .to_string()
176                    .as_ref()
177                    .remove_crlf(ctx.runtime.max_header_size)
178            })
179            .or_else(|| ctx.message.subject().map(|s| s.to_string()))
180            .unwrap_or_default();
181
182        let message = std::mem::take(&mut ctx.message);
183        #[cfg(test)]
184        let boundary = make_test_boundary();
185        #[cfg(not(test))]
186        let boundary = mail_builder::mime::make_boundary(".");
187
188        ctx.message_size += ((boundary.len() + 6) * 3) + body.len() + 2;
189        ctx.part = 0;
190        ctx.has_changes = true;
191        ctx.message = Message {
192            html_body: Vec::with_capacity(0),
193            text_body: Vec::with_capacity(0),
194            attachments: Vec::with_capacity(0),
195            parts: vec![
196                MessagePart {
197                    headers: vec![],
198                    is_encoding_problem: false,
199                    body: PartType::Multipart(vec![1, 2]),
200                    encoding: Encoding::None,
201                    offset_header: 0,
202                    offset_body: 0,
203                    offset_end: 0,
204                },
205                MessagePart {
206                    headers: vec![],
207                    is_encoding_problem: false,
208                    body: PartType::Text(body.into()),
209                    encoding: Encoding::QuotedPrintable, // Flag non-mime part
210                    offset_header: 0,
211                    offset_body: 0,
212                    offset_end: 0,
213                },
214                MessagePart {
215                    headers: vec![],
216                    is_encoding_problem: false,
217                    body: PartType::Message(message),
218                    encoding: Encoding::QuotedPrintable, // Flag non-mime part
219                    offset_header: 0,
220                    offset_body: 0,
221                    offset_end: 0,
222                },
223            ],
224            raw_message: b""[..].into(),
225        };
226
227        ctx.insert_header(
228            0,
229            HeaderName::Other("Content-Type".into()),
230            format!("multipart/mixed; boundary=\"{boundary}\""),
231            true,
232        );
233        ctx.insert_header(0, HeaderName::Other("Subject".into()), subject, true);
234        ctx.insert_header(
235            1,
236            HeaderName::Other("Content-Type".into()),
237            "text/plain; charset=utf-8",
238            true,
239        );
240        ctx.insert_header(
241            2,
242            HeaderName::Other("Content-Type".into()),
243            "message/rfc822",
244            true,
245        );
246
247        let mut add_date = true;
248        let mut add_message_id = true;
249        let mut add_from = true;
250
251        for header in &self.headers {
252            let header = ctx.eval_value(header);
253            if let Some((mut header_name, mut header_value)) =
254                header.to_string().as_ref().split_once(':')
255            {
256                header_name = header_name.trim();
257                header_value = header_value.trim();
258                if !header_value.is_empty() {
259                    if let Some(name) = HeaderName::parse(header_name) {
260                        if !ctx.runtime.protected_headers.contains(&name) {
261                            match &name {
262                                HeaderName::Date => {
263                                    add_date = false;
264                                }
265                                HeaderName::From => {
266                                    add_from = false;
267                                }
268                                HeaderName::MessageId => {
269                                    add_message_id = false;
270                                }
271                                _ => (),
272                            }
273
274                            ctx.insert_header(
275                                0,
276                                HeaderName::Other(header_name.to_string().into()),
277                                header_value.remove_crlf(ctx.runtime.max_header_size),
278                                true,
279                            );
280                        }
281                    }
282                }
283            }
284        }
285
286        if add_from {
287            ctx.insert_header(
288                0,
289                HeaderName::Other("From".to_string().into()),
290                ctx.user_from_field(),
291                true,
292            );
293        }
294
295        if add_date {
296            #[cfg(not(test))]
297            let header_value = mail_builder::headers::date::Date::now().to_rfc822();
298            #[cfg(test)]
299            let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string();
300
301            ctx.insert_header(
302                0,
303                HeaderName::Other("Date".to_string().into()),
304                header_value,
305                true,
306            );
307        }
308
309        if add_message_id {
310            let mut header_value = Vec::with_capacity(20);
311            #[cfg(not(test))]
312            generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap();
313            #[cfg(test)]
314            header_value.extend_from_slice(b"<auto-generated@message-id>");
315
316            ctx.insert_header(
317                0,
318                HeaderName::Other("Message-ID".to_string().into()),
319                String::from_utf8(header_value).unwrap(),
320                true,
321            );
322        }
323    }
324}
325
326impl ExtractText {
327    pub(crate) fn exec(&self, ctx: &mut Context) {
328        let mut value = String::new();
329
330        if !ctx.part_iter_stack.is_empty() {
331            match ctx.message.parts.get(ctx.part as usize).map(|p| &p.body) {
332                Some(PartType::Text(text)) => {
333                    value = if let Some(first) = &self.first {
334                        text.chars().take(*first).collect()
335                    } else {
336                        text.as_ref().to_string()
337                    };
338                }
339                Some(PartType::Html(html)) => {
340                    value = if let Some(first) = &self.first {
341                        html_to_text(html.as_ref()).chars().take(*first).collect()
342                    } else {
343                        html_to_text(html.as_ref())
344                    };
345                }
346                _ => (),
347            }
348
349            if !self.modifiers.is_empty() && !value.is_empty() {
350                for modifier in &self.modifiers {
351                    value = modifier.apply(&value, ctx);
352                }
353            }
354        }
355
356        match &self.name {
357            VariableType::Local(var_id) => {
358                if let Some(var) = ctx.vars_local.get_mut(*var_id) {
359                    *var = value.into();
360                } else {
361                    debug_assert!(false, "Non-existent local variable {var_id}");
362                }
363            }
364            VariableType::Global(var_name) => {
365                ctx.vars_global
366                    .insert(var_name.to_string().into(), value.into());
367            }
368            VariableType::Envelope(env) => {
369                ctx.add_set_envelope_event(*env, value);
370            }
371            _ => (),
372        }
373    }
374}
375
376enum StackItem<'x> {
377    Message(&'x Message<'x>),
378    Boundary(&'x str),
379    None,
380}
381
382impl Context<'_> {
383    pub(crate) fn build_message_id(&mut self) -> Option<Event> {
384        if self.has_changes {
385            self.last_message_id += 1;
386            self.main_message_id = self.last_message_id;
387            self.has_changes = false;
388            let message = self.build_message();
389            Some(Event::CreatedMessage {
390                message_id: self.main_message_id,
391                message,
392            })
393        } else {
394            None
395        }
396    }
397
398    pub(crate) fn build_message(&mut self) -> Vec<u8> {
399        let mut current_message = &self.message;
400        let mut current_boundary = "";
401        let mut message = Vec::with_capacity(self.message_size);
402        let mut iter = [0u32].iter();
403        let mut iter_stack = Vec::new();
404        let mut last_offset = 0;
405
406        'outer: loop {
407            while let Some(part) = iter
408                .next()
409                .and_then(|p| current_message.parts.get(*p as usize))
410            {
411                if last_offset > 0 {
412                    message.extend_from_slice(
413                        &current_message.raw_message
414                            [last_offset as usize..part.offset_header as usize],
415                    );
416                } else if !current_boundary.is_empty()
417                    && part.offset_end == 0
418                    && !matches!(iter_stack.last(), Some((StackItem::Message(_), _, _)))
419                {
420                    message.extend_from_slice(b"\r\n--");
421                    message.extend_from_slice(current_boundary.as_bytes());
422                    message.extend_from_slice(b"\r\n");
423                }
424
425                let mut ct_pos = usize::MAX;
426
427                for (header_pos, header) in part.headers.iter().enumerate() {
428                    if header.offset_end != 0 {
429                        if header.offset_field != header.offset_start {
430                            message.extend_from_slice(
431                                &current_message.raw_message
432                                    [header.offset_field as usize..header.offset_end as usize],
433                            );
434                        } else {
435                            // Renamed header
436                            message.extend_from_slice(header.name.as_str().as_bytes());
437                            message.extend_from_slice(b":");
438                            message.extend_from_slice(
439                                &current_message.raw_message
440                                    [header.offset_start as usize..header.offset_end as usize],
441                            );
442                        }
443                    } else {
444                        if header.name == HeaderName::Other("Content-Type".into()) {
445                            ct_pos = header_pos;
446                        }
447
448                        message.extend_from_slice(header.name.as_str().as_bytes());
449                        message.extend_from_slice(b": ");
450                        message.extend_from_slice(header.value.as_text().unwrap_or("").as_bytes());
451                        message.extend_from_slice(b"\r\n");
452                    }
453                }
454
455                if part.offset_body != 0 || part.encoding != Encoding::None {
456                    // Add CRLF unless this is a :mime replaced part
457                    message.extend_from_slice(b"\r\n");
458                }
459
460                if part.offset_body != 0 {
461                    // Original message part
462
463                    if let PartType::Multipart(subparts) = &part.body {
464                        // Multiparts contain offsets of the entire part, do not add.
465                        iter_stack.push((
466                            StackItem::None,
467                            part,
468                            std::mem::replace(&mut iter, subparts.iter()),
469                        ));
470                        last_offset = part.offset_body;
471                        continue 'outer;
472                    } else {
473                        message.extend_from_slice(
474                            &current_message.raw_message
475                                [part.offset_body as usize..part.offset_end as usize],
476                        )
477                    }
478                } else {
479                    match &part.body {
480                        PartType::Message(nested_message) => {
481                            // Enclosed message
482                            iter_stack.push((
483                                StackItem::Message(current_message),
484                                part,
485                                std::mem::replace(&mut iter, [0].iter()),
486                            ));
487                            current_message = nested_message;
488                            continue 'outer;
489                        }
490                        PartType::Multipart(subparts) => {
491                            // Multipart enclosing nested message, obtain MIME boundary
492                            let prev_boundary = std::mem::replace(
493                                &mut current_boundary,
494                                if ct_pos != usize::MAX {
495                                    part.headers[ct_pos]
496                                        .value
497                                        .as_text()
498                                        .and_then(|h| h.split_once("boundary=\""))
499                                        .and_then(|(_, h)| h.split_once('\"'))
500                                        .map(|(h, _)| h)
501                                } else {
502                                    None
503                                }
504                                .unwrap_or("invalid-boundary"),
505                            );
506
507                            // Enclose multipart
508                            iter_stack.push((
509                                StackItem::Boundary(prev_boundary),
510                                part,
511                                std::mem::replace(&mut iter, subparts.iter()),
512                            ));
513                            continue 'outer;
514                        }
515                        _ => {
516                            // Replaced part
517                            message.extend_from_slice(part.contents());
518                        }
519                    }
520                }
521                last_offset = part.offset_end;
522            }
523
524            if let Some((prev_item, prev_part, prev_iter)) = iter_stack.pop() {
525                match prev_item {
526                    StackItem::Message(prev_message) => {
527                        if last_offset > 0 {
528                            if let Some(bytes) =
529                                current_message.raw_message.get(last_offset as usize..)
530                            {
531                                message.extend_from_slice(bytes);
532                            }
533                            last_offset = 0;
534                        }
535                        current_message = prev_message;
536                    }
537                    StackItem::Boundary(prev_boundary) => {
538                        if !current_boundary.is_empty() {
539                            message.extend_from_slice(b"\r\n--");
540                            message.extend_from_slice(current_boundary.as_bytes());
541                            message.extend_from_slice(b"--\r\n");
542                        }
543                        current_boundary = prev_boundary;
544                    }
545                    StackItem::None => {
546                        message.extend_from_slice(
547                            &current_message.raw_message
548                                [last_offset as usize..prev_part.offset_end as usize],
549                        );
550                        last_offset = prev_part.offset_end;
551                    }
552                }
553                iter = prev_iter;
554            } else {
555                break;
556            }
557        }
558
559        if last_offset > 0 {
560            if let Some(bytes) = current_message.raw_message.get(last_offset as usize..) {
561                message.extend_from_slice(bytes);
562            }
563        }
564
565        message
566    }
567}
568
569#[cfg(test)]
570thread_local!(static COUNTER: std::cell::Cell<u64>  = 0.into());
571
572#[cfg(test)]
573pub(crate) fn make_test_boundary() -> String {
574    format!("boundary_{}", COUNTER.with(|c| { c.replace(c.get() + 1) }))
575}
576
577#[cfg(test)]
578pub(crate) fn reset_test_boundary() {
579    COUNTER.with(|c| c.replace(0));
580}