aboutsummaryrefslogtreecommitdiff
path: root/src/imap/session.rs
blob: fa3232a1d924f659fcb4ccbde337adbb845e390e (plain) (blame)
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anonymous, authenticated, selected};
use crate::imap::flow;
use crate::imap::request::Request;
use crate::imap::response::{Response, ResponseOrIdle};
use crate::login::ArcLoginProvider;
use anyhow::{anyhow, bail, Context, Result};
use imap_codec::imap_types::{command::Command, core::Tag};

//-----
pub struct Instance {
    pub login_provider: ArcLoginProvider,
    pub server_capabilities: ServerCapability,
    pub client_capabilities: ClientCapability,
    pub state: flow::State,
}
impl Instance {
    pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self {
        let client_cap = ClientCapability::new(&cap);
        Self {
            login_provider,
            state: flow::State::NotAuthenticated,
            server_capabilities: cap,
            client_capabilities: client_cap,
        }
    }

    pub async fn request(&mut self, req: Request) -> ResponseOrIdle {
        match req {
            Request::IdleStart(tag) => self.idle_init(tag),
            Request::IdlePoll => self.idle_poll().await,
            Request::ImapCommand(cmd) => self.command(cmd).await,
        }
    }

    pub fn idle_init(&mut self, tag: Tag<'static>) -> ResponseOrIdle {
        // Build transition
        //@FIXME the notifier should be hidden inside the state and thus not part of the transition!
        let transition = flow::Transition::Idle(tag.clone(), tokio::sync::Notify::new());

        // Try to apply the transition and get the stop notifier
        let maybe_stop = self
            .state
            .apply(transition)
            .context("IDLE transition failed")
            .and_then(|_| {
                self.state
                    .notify()
                    .ok_or(anyhow!("IDLE state has no Notify object"))
            });

        // Build an appropriate response
        match maybe_stop {
            Ok(stop) => ResponseOrIdle::IdleAccept(stop),
            Err(e) => {
                tracing::error!(err=?e, "unable to init idle due to a transition error");
                //ResponseOrIdle::IdleReject(tag)
                let no = Response::build()
                    .tag(tag)
                    .message(
                        "Internal error, processing command triggered an illegal IMAP state transition",
                    )
                    .no()
                    .unwrap();
                ResponseOrIdle::IdleReject(no)
            }
        }
    }

    pub async fn idle_poll(&mut self) -> ResponseOrIdle {
        match self.idle_poll_happy().await {
            Ok(r) => r,
            Err(e) => {
                tracing::error!(err=?e, "something bad happened in idle");
                ResponseOrIdle::Response(Response::bye().unwrap())
            }
        }
    }

    pub async fn idle_poll_happy(&mut self) -> Result<ResponseOrIdle> {
        let (mbx, tag, stop) = match &mut self.state {
            flow::State::Idle(_, ref mut mbx, _, tag, stop) => (mbx, tag.clone(), stop.clone()),
            _ => bail!("Invalid session state, can't idle"),
        };

        tokio::select! {
            _ = stop.notified() => {
                self.state.apply(flow::Transition::UnIdle)?;
                return Ok(ResponseOrIdle::Response(Response::build()
                    .tag(tag.clone())
                    .message("IDLE completed")
                    .ok()?))
            },
            change = mbx.idle_sync() => {
                tracing::debug!("idle event");
                return Ok(ResponseOrIdle::IdleEvent(change?));
            }
        }
    }

    pub async fn command(&mut self, cmd: Command<'static>) -> ResponseOrIdle {
        // Command behavior is modulated by the state.
        // To prevent state error, we handle the same command in separate code paths.
        let (resp, tr) = match &mut self.state {
            flow::State::NotAuthenticated => {
                let ctx = anonymous::AnonymousContext {
                    req: &cmd,
                    login_provider: &self.login_provider,
                    server_capabilities: &self.server_capabilities,
                };
                anonymous::dispatch(ctx).await
            }
            flow::State::Authenticated(ref user) => {
                let ctx = authenticated::AuthenticatedContext {
                    req: &cmd,
                    server_capabilities: &self.server_capabilities,
                    client_capabilities: &mut self.client_capabilities,
                    user,
                };
                authenticated::dispatch(ctx).await
            }
            flow::State::Selected(ref user, ref mut mailbox, ref perm) => {
                let ctx = selected::SelectedContext {
                    req: &cmd,
                    server_capabilities: &self.server_capabilities,
                    client_capabilities: &mut self.client_capabilities,
                    user,
                    mailbox,
                    perm,
                };
                selected::dispatch(ctx).await
            }
            flow::State::Idle(..) => Err(anyhow!("can not receive command while idling")),
            flow::State::Logout => Response::build()
                .tag(cmd.tag.clone())
                .message("No commands are allowed in the LOGOUT state.")
                .bad()
                .map(|r| (r, flow::Transition::None)),
        }
        .unwrap_or_else(|err| {
            tracing::error!("Command error {:?} occured while processing {:?}", err, cmd);
            (
                Response::build()
                    .to_req(&cmd)
                    .message("Internal error while processing command")
                    .bad()
                    .unwrap(),
                flow::Transition::None,
            )
        });

        if let Err(e) = self.state.apply(tr) {
            tracing::error!(
                "Transition error {:?} occured while processing on command {:?}",
                e,
                cmd
            );
            return ResponseOrIdle::Response(Response::build()
                .to_req(&cmd)
                .message(
                    "Internal error, processing command triggered an illegal IMAP state transition",
                )
                .bad()
                .unwrap());
        }
        ResponseOrIdle::Response(resp)

        /*match &self.state {
            flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()),
            _ => ResponseOrIdle::Response(resp),
        }*/
    }
}