arrows/common/
mail.rs

1#![allow(clippy::large_enum_variant)]
2use crate::Result;
3use crate::{
4    common::utils::{compute_hash, from_bytes, option_of_bytes},
5    Addr,
6};
7
8use serde::{Deserialize, Serialize};
9use std::mem::{replace, swap};
10use std::time::SystemTime;
11use uuid::Uuid;
12///The variants of actual message payload - Text, Binary blob or a Command adjoining an
13///Action
14
15#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
16pub enum Content {
17    Text(String),
18    Binary(Vec<u8>),
19    Command(Action),
20}
21impl std::fmt::Display for Action {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            action @ Self::Shutdown => write!(f, "{}", action.as_text()),
25            cont @ Self::Continue => write!(f, "{}", cont.as_text()),
26            Self::Echo(s) => write!(f, "Echo({})", s),
27        }
28    }
29}
30
31use Content::*;
32///The Mail enum which could be Trade(single message), Bulk(multiple messages) or Blank
33#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
34pub enum Mail {
35    ///A mail variant with a single message inside. Actor's receives this variant
36    Trade(Msg),
37    ///Contains multiple messages - used for buffering, single shot transmission over the wire
38    Bulk(Vec<Msg>),
39    ///An empty mail
40    Blank,
41}
42use Mail::*;
43
44impl Mail {
45    ///Get a handle to the inner message
46    pub fn message(&self) -> &Msg {
47        match self {
48            Trade(ref msg) => msg,
49            _ => panic!("message is supported only on Trade variant"),
50        }
51    }
52    ///Get a handle to inner messages without hollowing out the mail
53    pub fn messages(&self) -> &Vec<Msg> {
54        match self {
55            Bulk(ref msgs) => msgs,
56            _ => panic!("messages is supported only on Bulk variant"),
57        }
58    }
59    ///Take the inner message out - if its a Trade variant
60    pub fn take(self) -> Msg {
61        match self {
62            Trade(msg) => msg,
63            _ => panic!(),
64        }
65    }
66    ///Take all the content out if its Mail enum variant is Bulk
67    pub fn take_all(self) -> Vec<Msg> {
68        match self {
69            Bulk(msgs) => msgs,
70            _ => panic!(),
71        }
72    }
73    ///If the mail is actually a command - does it match a specific command
74    pub fn command_equals(&self, action: Action) -> bool {
75        if !self.is_command() {
76            return false;
77        }
78        match self {
79            Trade(_) => self.message().command_equals(action),
80            bulk @ Bulk(_) => bulk.messages()[0].command_equals(action),
81            _ => false,
82        }
83    }
84    ///Is the mail is actually a containing a single command like Shutdown etc?
85    pub fn is_command(&self) -> bool {
86        match self {
87            trade @ Trade(_) => trade.message().is_command(),
88            bulk @ Bulk(ref _msgs)
89                if bulk.messages().len() == 1 && bulk.messages()[0].is_command() =>
90            {
91                true
92            }
93            _ => false,
94        }
95    }
96
97    ///Get the embedded [Action](crate::Action) out if this mail is a command
98    ///
99    pub fn action(&self) -> Option<Action> {
100        if !self.is_command() {
101            return None;
102        }
103        match self {
104            Trade(msg) => msg.action(),
105            bulk @ Bulk(_) => bulk.messages()[0].action(),
106            _ => None,
107        }
108    }
109
110    //Actors are whimsical - might not respond immediately
111    //Convert the buffered responses into single whole mail
112    pub(crate) fn fold(mails: Vec<Option<Mail>>) -> Mail {
113        Bulk(
114            mails
115                .into_iter()
116                .map(Mail::from)
117                .flat_map(|mail| match mail {
118                    trade @ Trade(_) => vec![trade.take()],
119                    bulk @ Bulk(_) => bulk.take_all(),
120                    _ => unreachable!(),
121                })
122                .collect::<Vec<_>>(),
123        )
124    }
125    ///Is the mail empty - mostly to avoid transmitting
126    pub fn is_blank(mail: &Mail) -> bool {
127        matches!(mail, Blank)
128    }
129    //Checks only for the variant of Trade!
130    pub(crate) fn inbound(mail: &Mail) -> bool {
131        match mail {
132            Trade(ref msg) => msg.inbound(),
133            _ => false,
134        }
135    }
136    //Split into inbound and outbound(local vs remote)
137    pub(crate) fn split(mail: Mail) -> Option<(Vec<Msg>, Vec<Msg>)> {
138        match mail {
139            Blank => None,
140            trade @ Trade(_) if Mail::inbound(&trade) => Some((vec![trade.take()], Vec::new())),
141            trade @ Trade(_) if !Mail::inbound(&trade) => Some((Vec::new(), vec![trade.take()])),
142            Trade(_) => unreachable!(),
143            Bulk(msgs) => match msgs.into_iter().partition::<Vec<Msg>, _>(Msg::inbound) {
144                (v1, v2) if v1.is_empty() & v2.is_empty() => None,
145                or_else @ (_, _) => Some(or_else),
146            },
147        }
148    }
149
150    //partition as inbound/outbound messages
151    pub(crate) fn partition(mails: Vec<Option<Mail>>) -> Option<(Vec<Msg>, Vec<Msg>)> {
152        match mails
153            .into_iter()
154            .map(Mail::from)
155            .filter(|mail| !Mail::is_blank(mail))
156            .flat_map(|mail| match mail {
157                trade @ Trade(_) => vec![trade.take()],
158                Bulk(msgs) => msgs,
159                _ => panic!(),
160            })
161            .partition::<Vec<Msg>, _>(Msg::inbound)
162        {
163            (v1, v2) if v1.is_empty() & v2.is_empty() => None,
164            or_else @ (_, _) => Some(or_else),
165        }
166    }
167    ///Set from address on all the messagess inside a potential mail
168    pub fn set_from(mail: &mut Option<Mail>, from: &Addr) {
169        match mail {
170            Some(mail) => match mail {
171                Trade(msg) => msg.set_from(from),
172                Bulk(msgs) => {
173                    for msg in msgs.iter_mut() {
174                        msg.set_from(from);
175                    }
176                }
177                Blank => (),
178            },
179            None => (),
180        }
181    }
182}
183
184impl From<Option<Mail>> for Mail {
185    fn from(opt: Option<Mail>) -> Self {
186        match opt {
187            Some(mail) => mail,
188            None => Mail::Blank,
189        }
190    }
191}
192
193impl From<Vec<Msg>> for Mail {
194    fn from(msgs: Vec<Msg>) -> Self {
195        Bulk(msgs)
196    }
197}
198///[Msg](crate::Msg) content type can also be Command. `Action` represents tasks
199///corresponding to Commands.
200#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
201#[non_exhaustive]
202pub enum Action {
203    ///Shutdown the message listener
204    Shutdown,
205    ///Send an echo message to the listener
206    Echo(String),
207    ///Good to go, carry on
208    Continue,
209}
210impl Action {
211    //Meant for internal use by the system
212    fn as_text(&self) -> &str {
213        match self {
214            Self::Shutdown => "Shutdown",
215            Self::Echo(_) => "Echo",
216            Self::Continue => "Continue",
217        }
218    }
219    ///Inner content such as echo message
220    pub fn inner(&self) -> &str {
221        match self {
222            Self::Shutdown => "",
223            Self::Continue => "",
224            Self::Echo(s) => s,
225        }
226    }
227
228    ///Execute the action embedded in a Mail whose content  might be a Command
229    ///For echo action execute just reverses the incoming text
230    pub fn execute(&mut self, _input: Mail) -> Option<Mail> {
231        match self {
232            Self::Echo(text) => Some(Msg::echo(&text.chars().rev().collect::<String>()).into()),
233            _ => None,
234        }
235    }
236}
237///The actual payload received by actors inside a Mail enum construct
238#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Default)]
239pub struct Msg {
240    id: u64,
241    from: Option<Addr>,
242    to: Option<Addr>,
243    content: Option<Content>,
244    dispatched: Option<SystemTime>,
245}
246
247impl Msg {
248    ///Construct a new message with binary content with from and to addresses
249    pub fn new(content: Option<Vec<u8>>, from: &str, to: &str) -> Self {
250        Self {
251            id: compute_hash(&Uuid::new_v4()),
252            from: Some(Addr::new(from)),
253            to: Some(Addr::new(to)),
254            content: content.map(Binary),
255            dispatched: None,
256        }
257    }
258    ///Update the message with binary content
259    pub fn with_binary_content(content: Option<Vec<u8>>) -> Self {
260        Self {
261            id: compute_hash(&Uuid::new_v4()),
262            from: None,
263            to: None,
264            content: content.map(Binary),
265            dispatched: None,
266        }
267    }
268
269    ///Create a msg with content as text to an actor(`actor1`) in the local system
270    ///
271    ///
272    /// # Example
273    ///
274    ///```
275    ///use arrows::send;
276    ///use arrows::Msg;
277    ///
278    ///let m = Msg::with_text("A good will message");
279    ///send!("actor1", m);
280    ///
281    ///
282    pub fn from_text(content: &str) -> Self {
283        Self {
284            id: compute_hash(&Uuid::new_v4()),
285            from: None,
286            to: None,
287            content: Some(Text(content.to_string())),
288            dispatched: Some(SystemTime::now()),
289        }
290    }
291    ///Construct a text message with from and to addresses
292    pub fn with_text(content: &str, from: &str, to: &str) -> Self {
293        Self {
294            id: compute_hash(&Uuid::new_v4()),
295            from: Some(Addr::new(from)),
296            to: Some(Addr::new(to)),
297            content: Some(Text(content.to_string())),
298            dispatched: Some(SystemTime::now()),
299        }
300    }
301    /// Get the content of msg as text. In case - binary content being actually binary
302    /// this would not be helpful.
303    pub fn as_text(&self) -> Option<&str> {
304        match self.content {
305            Some(Binary(ref bytes)) => {
306                let text: Result<&str> = from_bytes(bytes);
307                text.ok()
308            }
309            Some(Text(ref s)) => Some(s),
310            Some(Command(ref action)) => Some(action.inner()),
311            None => None,
312        }
313    }
314    ///Is the message actually a command?
315    pub fn is_command(&self) -> bool {
316        matches!(self.content, Some(Command(_)))
317    }
318    ///Command action equality check
319    pub fn command_equals(&self, action: Action) -> bool {
320        if !self.is_command() {
321            return false;
322        }
323        if let Some(Content::Command(ref own_action)) = self.content {
324            return own_action.as_text() == action.as_text();
325        }
326        false
327    }
328    ///Get the embedded [Action](crate::Action) out if this [Msg](crate::Msg) content
329    ///is really is Command
330    pub fn action(&self) -> Option<Action> {
331        if !self.is_command() {
332            return None;
333        }
334        match self.content {
335            Some(Command(ref action)) => Some(action.clone()),
336            _ => None,
337        }
338    }
339
340    ///The message as bytes - irrespective of whether content is text or
341    ///actual binary blob. Empty byte vec - if can not be serialized
342    pub fn as_bytes(&self) -> Vec<u8> {
343        option_of_bytes(self).unwrap_or_default()
344    }
345    ///Construct a text reply with content as string and message direction reversed
346    pub fn text_reply(&mut self, reply: &str) {
347        swap(&mut self.from, &mut self.to);
348        let _ignore = replace(&mut self.content, Some(Text(reply.to_string())));
349    }
350
351    ///Update the content of the message - text
352    pub fn update_text_content(&mut self, reply: &str) {
353        let _ignore = replace(&mut self.content, Some(Text(reply.to_string())));
354    }
355    ///Set new binary content and new local recipient actor address
356    pub fn with_content_and_to(&mut self, new_content: Vec<u8>, new_to: &str) {
357        self.content = Some(Binary(new_content));
358        self.to = Some(Addr::new(new_to));
359    }
360    ///Set the binary content of the message
361    pub fn with_content(&mut self, new_content: Vec<u8>) {
362        self.content = Some(Binary(new_content));
363    }
364    ///Set the recipient address of the message
365    pub fn set_recipient_addr(&mut self, addr: &Addr) {
366        self.to = Some(addr.clone());
367    }
368    ///Set the recipient identifier as string literal
369    pub fn set_recipient(&mut self, new_to: &str) {
370        self.to = Some(Addr::new(new_to));
371    }
372    ///Set the recipient actor's IP - used in remoting
373    pub fn set_recipient_ip(&mut self, new_to_ip: &str) {
374        if let Some(ref mut addr) = self.to {
375            addr.with_ip(new_to_ip);
376        }
377    }
378    ///Set the recipient port
379    pub fn set_recipient_port(&mut self, new_port: u16) {
380        if let Some(ref mut addr) = self.to {
381            addr.with_port(new_port);
382        }
383    }
384    ///Get the port of the recipient actor - used for remote messaging
385    pub fn get_recipient_port(&self) -> u16 {
386        if let Some(addr) = &self.to {
387            return addr.get_port().expect("port");
388        }
389        0
390    }
391    ///Reverse the message direction - 'to' to 'from' or other way
392    pub fn binary_reply(&mut self, reply: Option<Vec<u8>>) {
393        swap(&mut self.from, &mut self.to);
394        let _ignore = replace(&mut self.content, reply.map(Binary));
395    }
396    ///Get the binary content - the message remains intact
397    pub fn binary_content(&self) -> Option<Vec<u8>> {
398        match &self.content {
399            Some(Binary(data)) => Some(data.to_vec()),
400            _ => None,
401        }
402    }
403    ///If the message content is binary blob - get it
404    ///Would take content out - leaving message content to a None
405    pub fn binary_content_out(&mut self) -> Option<Vec<u8>> {
406        match self.content.take() {
407            Some(Binary(data)) => Some(data),
408            content @ Some(_) => {
409                self.content = content;
410                None
411            }
412            _ => None,
413        }
414    }
415    ///Get the address of the actor the message is directed at
416    pub fn get_to(&self) -> &Option<Addr> {
417        &self.to
418    }
419    ///Get the id
420    pub fn get_id(&self) -> &u64 {
421        &self.id
422    }
423    ///Get the id as string. Required because unique ids overflow i64 range supported by
424    ///the backing store
425    pub fn id_as_string(&self) -> String {
426        self.get_id().to_string()
427    }
428
429    ///Get the unique id of the actor message is directed at
430    pub fn get_to_id(&self) -> u64 {
431        match self.to {
432            Some(ref addr) => addr.get_id(),
433            None => 0,
434        }
435    }
436
437    ///Get the from address
438    pub fn get_from(&self) -> &Option<Addr> {
439        &self.from
440    }
441    ///Check if message is directed at any actor in the system
442    pub fn inbound(&self) -> bool {
443        match self.to {
444            Some(ref addr) => addr.is_local(),
445            None => false,
446        }
447    }
448    ///Set the from address of a message - specific usage while sending out actor message
449    ///processing outcome
450    pub fn set_from(&mut self, from: &Addr) {
451        let _ignore = std::mem::replace(&mut self.from, Some(from.clone()));
452    }
453
454    ///Construct a Shutdown command to shutdown the system listener
455    pub fn shutdown() -> Self {
456        let mut cmd = Msg::default();
457        let _ignore = std::mem::replace(&mut cmd.content, Some(Content::Command(Action::Shutdown)));
458        cmd
459    }
460    ///Construct a Echo command to send to the system listener to check its liveness
461    pub fn echo(s: &str) -> Self {
462        let mut cmd = Msg::default();
463        let _ignore = std::mem::replace(
464            &mut cmd.content,
465            Some(Content::Command(Action::Echo(s.to_string()))),
466        );
467        cmd
468    }
469}
470
471impl Default for Mail {
472    fn default() -> Self {
473        Mail::Blank
474    }
475}
476
477impl From<Msg> for Mail {
478    fn from(msg: Msg) -> Self {
479        Mail::Trade(msg)
480    }
481}
482
483//A mail with extra details - inbound/outbound, seq, from & destined to
484pub(crate) enum RichMail {
485    RichContent(Mail, bool, i64, Option<Addr>, Option<Addr>),
486}
487use RichMail::RichContent;
488impl RichMail {
489    pub(crate) fn mail(&self) -> &Mail {
490        let RichContent(mail, _, _, _, _) = self;
491        mail
492    }
493    pub(crate) fn mail_out(&mut self) -> Mail {
494        let RichContent(mail, _, _, _, _) = self;
495        std::mem::replace(mail, Mail::Blank)
496    }
497
498    pub(crate) fn replace_mail(&mut self, msgs: Vec<Msg>) {
499        let RichContent(mail, _, _, _, _) = self;
500        *mail = Mail::Bulk(msgs);
501    }
502    pub(crate) fn to(&self) -> Option<&Addr> {
503        let RichContent(_, _, _, _, to) = self;
504        to.as_ref()
505    }
506    pub(crate) fn from(&self) -> Option<&Addr> {
507        let RichContent(_, _, _, from, _) = self;
508        from.as_ref()
509    }
510    pub(crate) fn inbound(&self) -> bool {
511        let RichContent(_, inbound, _, _, _) = self;
512        *inbound
513    }
514    pub(crate) fn seq(&self) -> i64 {
515        let RichContent(_, _, seq, _, _) = self;
516        *seq
517    }
518}
519
520impl std::fmt::Display for Msg {
521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522        {
523            write!(f, "Msg({}), ", &self.id)?;
524            let _rs = match self.from {
525                Some(ref from) => write!(f, "from: {}, ", from),
526                None => write!(f, "from: None, "),
527            };
528            let _rs = match self.to {
529                Some(ref to) => write!(f, "to: {}, ", to),
530                None => write!(f, "to: None, "),
531            };
532            match self.content {
533                Some(ref content) => write!(f, "content: {}", content),
534                None => write!(f, "content: None"),
535            }
536        }
537    }
538}
539
540impl std::fmt::Display for Content {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        match self {
543            Text(text) => {
544                write!(f, "Text({})", text)
545            }
546            Binary(binary) => {
547                write!(f, "Binary(..) -> length {}", binary.len())
548            }
549            Command(c) => {
550                write!(f, "Command({})", c)
551            }
552        }
553    }
554}
555
556impl std::fmt::Display for Mail {
557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558        match self {
559            Trade(msg) => {
560                write!(f, "Trade({})", msg)
561            }
562            Bulk(ref msgs) => {
563                writeln!(f, "Bulk({})", msgs.len())?;
564                if !msgs.is_empty() {
565                    for msg in msgs.iter().take(msgs.len() - 1) {
566                        let _ = write!(f, "{}", msg);
567                        let _rs = writeln!(f);
568                    }
569                    write!(f, "{}", msgs[msgs.len() - 1])
570                } else {
571                    write!(f, " Empty")
572                }
573            }
574            Blank => write!(f, "Blank"),
575        }
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::common::utils::{from_bytes, option_of_bytes};
583
584    #[test]
585    fn create_trade_msg_test_content_and_to() {
586        let mut msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
587        assert_eq!(
588            from_bytes(&msg.binary_content_out().unwrap()).ok(),
589            Some("Content")
590        );
591        assert_eq!(msg.get_to(), &Some(Addr::new("addr_to")));
592        println!("{}", msg);
593    }
594
595    #[test]
596    fn create_trade_msg_test_from() {
597        let msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
598        assert_eq!(msg.get_from(), &Some(Addr::new("addr_from")));
599        println!("{}", msg);
600    }
601
602    #[test]
603    fn create_trade_msg_test_alter_content_and_to() {
604        let mut msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
605        assert_eq!(msg.binary_content(), option_of_bytes(&"Content"));
606        assert_eq!(msg.get_to(), &Some(Addr::new("addr_to")));
607        msg.with_content_and_to(option_of_bytes(&"New content").unwrap(), "New_to");
608        assert_eq!(msg.binary_content(), option_of_bytes(&"New content"));
609        assert_eq!(msg.get_to(), &Some(Addr::new("New_to")));
610    }
611
612    #[test]
613    fn set_recipient_test_1() {
614        let mut msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
615        assert_eq!(msg.get_to(), &Some(Addr::new("addr_to")));
616        msg.set_recipient("The new recipient");
617        assert_eq!(msg.get_to(), &Some(Addr::new("The new recipient")));
618    }
619
620    #[test]
621    fn binary_reply_test_1() {
622        let mut msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
623        msg.binary_reply(option_of_bytes(&"Reply"));
624        assert_eq!(msg.get_to(), &Some(Addr::new("addr_from")));
625        assert_eq!(msg.get_from(), &Some(Addr::new("addr_to")));
626        assert_eq!(msg.binary_content(), option_of_bytes(&"Reply"));
627    }
628    #[test]
629    fn text_reply_test_1() {
630        let mut msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
631        assert_eq!(msg.binary_content(), option_of_bytes(&"Content"));
632        msg.text_reply("Reply");
633        assert_eq!(msg.get_to(), &Some(Addr::new("addr_from")));
634        assert_eq!(msg.get_from(), &Some(Addr::new("addr_to")));
635        assert_eq!(msg.binary_content(), option_of_bytes(&"Reply"));
636    }
637    #[test]
638    fn as_text_test_1() {
639        let mut msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
640        assert_eq!(msg.binary_content(), option_of_bytes(&"Content"));
641        assert_eq!(msg.as_text(), Some("Content"));
642
643        msg.update_text_content("Updated content");
644        assert_eq!(msg.binary_content(), option_of_bytes(&"Updated content"));
645        assert_eq!(msg.as_text(), Some("Updated content"));
646    }
647    #[test]
648    fn outbound_mgs_test_1() {
649        let mut trade_msg = Msg::new(option_of_bytes(&"Content"), "addr_from", "addr_to");
650        assert!(trade_msg.inbound());
651
652        trade_msg.set_recipient_ip("89.89.89.89");
653        assert!(!trade_msg.inbound());
654    }
655    #[test]
656    fn test_mail_partition() {
657        let mut mails = vec![
658            Some(Mail::Blank),
659            Some(Mail::Blank),
660            None,
661            Some(Trade(Msg::with_text("mail", "from", "to"))),
662        ];
663
664        let mut m1 = Msg::with_text("mail", "from1", "to1");
665        let mut addr1 = Addr::new("add1");
666        addr1.with_port(9999);
667        m1.set_recipient_addr(&addr1);
668        mails.push(Some(Trade(m1)));
669        let mut addr2 = addr1.clone();
670        addr2.with_port(1111);
671        let mut m2 = Msg::with_text("mail", "from2", "to2");
672        m2.set_recipient_addr(&addr2);
673        mails.push(Some(Bulk(vec![m2])));
674
675        let mut m3 = Msg::with_text("mail", "from3", "to3");
676        m3.set_recipient_ip("89.89.89.89");
677        mails.push(Some(Bulk(vec![m3])));
678
679        if let Some((ref v1, ref v2)) = Mail::partition(mails) {
680            v1.iter().for_each(|msg| {
681                if let Some(ref addr) = msg.get_to() {
682                    assert!(addr.is_local());
683                }
684            });
685
686            v2.iter().for_each(|msg| {
687                if let Some(ref addr) = msg.get_to() {
688                    assert!(!addr.is_local());
689                }
690            });
691        }
692    }
693
694    #[test]
695    fn mail_print_test() {
696        let m = Msg::with_text("mail", "from", "to");
697        let mail = Mail::Trade(m);
698        println!("{}", mail);
699
700        let m1 = Msg::with_text("mail", "from", "to");
701        let m2 = Msg::with_text("mail", "from", "to");
702        let m3 = Msg::with_text("mail", "from", "to");
703        let bulk_mail = Mail::Bulk(vec![]);
704        println!("{}", bulk_mail);
705
706        let bulk_mail = Mail::Bulk(Vec::new());
707        println!("{}", bulk_mail);
708
709        let bulk_mail = Mail::Bulk(vec![m1, m2, m3]);
710        println!("Bulk {}", bulk_mail);
711
712        let m1 = Msg::with_text("mail", "from", "to");
713        let bulk_mail = Mail::Bulk(vec![m1]);
714        println!("Bulk {}", bulk_mail);
715
716        println!("Bulk {}", Mail::Blank);
717    }
718
719    #[test]
720    fn mail_is_command_test_1() {
721        let trade_mail: Mail = Msg::with_text("Some text", "from", "to").into();
722        assert!(!trade_mail.is_command());
723
724        let bulk = Mail::Bulk(vec![Msg::with_text("Some text", "from", "to")]);
725        assert!(!bulk.is_command());
726    }
727}