├── .gitignore ├── .travis-cargo-after.sh ├── .travis-cargo.sh ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── core ├── Cargo.lock ├── Cargo.toml └── src │ ├── command │ ├── fetch.rs │ ├── mod.rs │ ├── sequence_set.rs │ └── store.rs │ ├── error.rs │ ├── folder.rs │ ├── main.rs │ ├── message.rs │ ├── parser │ ├── error.rs │ ├── grammar │ │ ├── fetch.rs │ │ ├── mod.rs │ │ └── sequence.rs │ └── mod.rs │ ├── server │ ├── config.rs │ ├── imap.rs │ ├── lmtp.rs │ ├── mod.rs │ └── user │ │ ├── auth.rs │ │ ├── email.rs │ │ ├── login.rs │ │ └── mod.rs │ └── util.rs ├── maildir ├── cur │ ├── 1416716125 │ └── 1416546579:2,FS └── new │ └── 1414871673 ├── mime ├── Cargo.lock ├── Cargo.toml └── src │ ├── command.rs │ ├── error.rs │ └── lib.rs ├── reset.sh ├── telnet ├── telnet.py └── telnet.sh └── users.json.example /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *# 4 | 5 | # Rust 6 | *.o 7 | *.so 8 | *.swp 9 | *.dylib 10 | *.dSYM 11 | *.dll 12 | *.rlib 13 | *.dummy 14 | *.exe 15 | 16 | # Cargo 17 | /target/ 18 | /core/target/ 19 | /mime/target/ 20 | 21 | # LaTeX 22 | *.aux 23 | *.dvi 24 | *.fdb_latexmk 25 | *.fls 26 | *.log 27 | *.pdf 28 | *.sty 29 | 30 | # Umple 31 | umple.jar 32 | imapcd.gv 33 | imapuml.svg 34 | imapuml.pdf_tex 35 | 36 | # segimtp 37 | config.toml 38 | *.json 39 | .lock 40 | 41 | # Python 42 | .venv 43 | -------------------------------------------------------------------------------- /.travis-cargo-after.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PATH=$HOME/.local/bin/:$PATH 4 | 5 | travis-cargo --only stable doc-upload 6 | travis-cargo coveralls --no-sudo --verify 7 | -------------------------------------------------------------------------------- /.travis-cargo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PATH=$HOME/.local/bin/:$PATH 4 | 5 | travis-cargo build 6 | travis-cargo test 7 | travis-cargo bench 8 | travis-cargo --only stable doc 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | addons: 4 | apt: 5 | packages: 6 | - libcurl4-openssl-dev 7 | - libelf-dev 8 | - libdw-dev 9 | - binutils-dev 10 | rust: 11 | - nightly 12 | - beta 13 | - stable 14 | os: 15 | - linux 16 | cache: cargo 17 | before_script: 18 | - pip install 'travis-cargo<0.2' --user 19 | - export PATH=$HOME/.local/bin/:$PATH 20 | script: 21 | - (cd core && ../.travis-cargo.sh) 22 | - (cd mime && ../.travis-cargo.sh) 23 | after_success: 24 | - (cd core && ../.travis-cargo-after.sh); 25 | - (cd mime && ../.travis-cargo-after.sh); 26 | env: 27 | global: 28 | - RUST_BACKTRACE=1 29 | - TRAVIS_CARGO_NIGHTLY_FEATURE="" 30 | - secure: kQMciglCLiHWhwds9z7coVEU5+8Qp0t78ab4b58h2T09osuI+wW6FWcO4K0GIh9uAo+7QQ9WAPQVwBPZr8md63+QGZYz4L0EFoRSQrEZazR+IZgWI1I0DSOxEwCEwO5o5hJonKcOEFOIIopL9+11LMzlE9XtoB/hPFN1PijTvmc= 31 | notifications: 32 | email: false 33 | matrix: 34 | allow_failures: 35 | - rust: nightly 36 | fast_finish: true 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2014-2015, 2017 Nikita Pekin 4 | Copyright 2014-2017 William Pearson 5 | Copyright 2016 Google Inc. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | clean: 4 | rm -rf *.aux *.dvi *.fdb_latexmk *.fls *.log 5 | rm -rf target/ 6 | 7 | pdf: 8 | pdflatex --shell-escape outline.tex 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SEGIMAP 2 | ======= 3 | 4 | This is an IMAP server implementation written in Rust. It originally started out as a class project in Fall 2014. 5 | There is also an LMTP server attached so that SMTP servers may deliver mail without modifying the maildir themselves. 6 | 7 | [![Travis CI status](https://travis-ci.org/uiri/SEGIMAP.svg?branch=master)](https://travis-ci.org/uiri/SEGIMAP) 8 | 9 | Some notes about Rust 10 | --------------------- 11 | 12 | The most confusing thing for those reading the code who are unfamiliar with rust will likely be &str and String. &str is an actual string while String is actually a StringBuffer. `&x[..]` is used to get a &str out of a String called `x` and `y.to_string()` is used to get a String out of a &str called `y`. Sometimes we need something which is neither of these to be a string so we use `.to_str()` or `.to_string()` as appropriate. Sometimes we need a String but the thing we have can only be converted to &str so we have to chain the calls like so: `.to_str().to_string()`. It sometimes also occurs the other way around in which case we wind up with something like `&z.to_string()[..]`. 13 | 14 | & denotes a pointer (ie: pass-by-reference semantics). When you see &mut it means that the pointer is mutable. Rust only allows one mutable pointer at a time and enforces this at compile time. However, multiple immutable pointers may be created. * is used to dereference a pointer. In most cases, method calls will automatically dereference when needed. 15 | 16 | The statement `return thing;` is equivalent to `thing` (note the absence of a semi-colon). 17 | 18 | That should be everything someone who doesn't know rust needs to understand this code and why we do things certain ways. 19 | 20 | Some notes about IMAP 21 | --------------------- 22 | 23 | IMAP has three states: unauthenticated (before log in), authenticated and selected. The most important commands for actually reading mail are log in, to get to an authenticated state; list, to get the list of folders; select, to get to the selected state; fetch, to get data and metadata about the messages; and store, to modify the flags (metadata) of the messages. 24 | 25 | ### Relevant RFCS: 26 | 27 | [RFC 3501 - IMAP4rev1](http://tools.ietf.org/html/rfc3501) 28 | [RFC 2822 - Internet message format](http://tools.ietf.org/html/rfc2822) 29 | [RFC 2033 - LMTP](http://tools.ietf.org/html/rfc2033) 30 | [RFC 2821 - SMTP (LMTP is based heavily on SMTP) ](http://tools.ietf.org/html/rfc2821) 31 | [RFC 2045 - MIME Part 1](http://tools.ietf.org/html/rfc2045) 32 | [RFC 2046 - MIME Part 2](http://tools.ietf.org/html/rfc2046) 33 | 34 | Installing, building, running 35 | ----------------------------- 36 | 37 | Grab rust v0.12 38 | Grab cargo 39 | Run `cargo run` (alternatively, if you just want to compile the program, run `cargo build`) 40 | -------------------------------------------------------------------------------- /core/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "segimap" 3 | version = "0.0.1" 4 | dependencies = [ 5 | "bufstream 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "clippy 0.0.133 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 8 | "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "nom 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 10 | "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 11 | "openssl 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", 12 | "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 13 | "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "segimap_mime 0.0.1", 16 | "serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "serde_derive 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 18 | "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 20 | "toml 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 21 | "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", 22 | ] 23 | 24 | [[package]] 25 | name = "aho-corasick" 26 | version = "0.6.3" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | dependencies = [ 29 | "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 30 | ] 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "0.8.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | 37 | [[package]] 38 | name = "bufstream" 39 | version = "0.1.3" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | 42 | [[package]] 43 | name = "cargo_metadata" 44 | version = "0.2.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | dependencies = [ 47 | "serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 48 | "serde_derive 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 49 | "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 50 | ] 51 | 52 | [[package]] 53 | name = "clippy" 54 | version = "0.0.133" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | dependencies = [ 57 | "cargo_metadata 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 58 | "clippy_lints 0.0.133 (registry+https://github.com/rust-lang/crates.io-index)", 59 | ] 60 | 61 | [[package]] 62 | name = "clippy_lints" 63 | version = "0.0.133" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | dependencies = [ 66 | "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 67 | "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 71 | "serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 72 | "serde_derive 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 73 | "toml 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 75 | ] 76 | 77 | [[package]] 78 | name = "dtoa" 79 | version = "0.4.1" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | 82 | [[package]] 83 | name = "env_logger" 84 | version = "0.4.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | dependencies = [ 87 | "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 89 | ] 90 | 91 | [[package]] 92 | name = "foreign-types" 93 | version = "0.2.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | 96 | [[package]] 97 | name = "gcc" 98 | version = "0.3.45" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | 101 | [[package]] 102 | name = "gdi32-sys" 103 | version = "0.2.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | dependencies = [ 106 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 107 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 108 | ] 109 | 110 | [[package]] 111 | name = "itoa" 112 | version = "0.3.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | 115 | [[package]] 116 | name = "kernel32-sys" 117 | version = "0.2.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | dependencies = [ 120 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 121 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 122 | ] 123 | 124 | [[package]] 125 | name = "lazy_static" 126 | version = "0.2.8" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | 129 | [[package]] 130 | name = "libc" 131 | version = "0.2.22" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | 134 | [[package]] 135 | name = "log" 136 | version = "0.3.7" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | 139 | [[package]] 140 | name = "matches" 141 | version = "0.1.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | 144 | [[package]] 145 | name = "memchr" 146 | version = "1.0.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | dependencies = [ 149 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 150 | ] 151 | 152 | [[package]] 153 | name = "nom" 154 | version = "3.0.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | 157 | [[package]] 158 | name = "num" 159 | version = "0.1.37" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | dependencies = [ 162 | "num-bigint 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 163 | "num-complex 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 164 | "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", 166 | "num-rational 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "num-bigint" 172 | version = "0.1.37" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | dependencies = [ 175 | "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 176 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 177 | "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 178 | "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", 179 | ] 180 | 181 | [[package]] 182 | name = "num-complex" 183 | version = "0.1.37" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | dependencies = [ 186 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 187 | "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", 188 | ] 189 | 190 | [[package]] 191 | name = "num-integer" 192 | version = "0.1.34" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | dependencies = [ 195 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 196 | ] 197 | 198 | [[package]] 199 | name = "num-iter" 200 | version = "0.1.33" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | dependencies = [ 203 | "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 204 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 205 | ] 206 | 207 | [[package]] 208 | name = "num-rational" 209 | version = "0.1.36" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | dependencies = [ 212 | "num-bigint 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 213 | "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 214 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 215 | "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", 216 | ] 217 | 218 | [[package]] 219 | name = "num-traits" 220 | version = "0.1.37" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | 223 | [[package]] 224 | name = "openssl" 225 | version = "0.9.12" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | dependencies = [ 228 | "bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", 229 | "foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 230 | "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 231 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "openssl-sys 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)", 233 | ] 234 | 235 | [[package]] 236 | name = "openssl-sys" 237 | version = "0.9.12" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | dependencies = [ 240 | "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", 241 | "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 242 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 243 | "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 244 | "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 245 | ] 246 | 247 | [[package]] 248 | name = "pkg-config" 249 | version = "0.3.9" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | 252 | [[package]] 253 | name = "quine-mc_cluskey" 254 | version = "0.2.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | 257 | [[package]] 258 | name = "quote" 259 | version = "0.3.15" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | 262 | [[package]] 263 | name = "rand" 264 | version = "0.3.15" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | dependencies = [ 267 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 268 | ] 269 | 270 | [[package]] 271 | name = "redox_syscall" 272 | version = "0.1.17" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | 275 | [[package]] 276 | name = "regex" 277 | version = "0.2.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | dependencies = [ 280 | "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", 281 | "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 282 | "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 283 | "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 284 | "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 285 | ] 286 | 287 | [[package]] 288 | name = "regex-syntax" 289 | version = "0.4.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | 292 | [[package]] 293 | name = "rust-crypto" 294 | version = "0.2.36" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | dependencies = [ 297 | "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", 298 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 299 | "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 300 | "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", 301 | "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 302 | ] 303 | 304 | [[package]] 305 | name = "rustc-serialize" 306 | version = "0.3.24" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | 309 | [[package]] 310 | name = "same-file" 311 | version = "0.1.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | dependencies = [ 314 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 315 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 316 | ] 317 | 318 | [[package]] 319 | name = "segimap_mime" 320 | version = "0.0.1" 321 | 322 | [[package]] 323 | name = "semver" 324 | version = "0.6.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | dependencies = [ 327 | "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 328 | ] 329 | 330 | [[package]] 331 | name = "semver-parser" 332 | version = "0.7.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | 335 | [[package]] 336 | name = "serde" 337 | version = "1.0.2" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | 340 | [[package]] 341 | name = "serde_derive" 342 | version = "1.0.2" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | dependencies = [ 345 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 346 | "serde_derive_internals 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", 347 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 348 | ] 349 | 350 | [[package]] 351 | name = "serde_derive_internals" 352 | version = "0.15.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | dependencies = [ 355 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 356 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 357 | ] 358 | 359 | [[package]] 360 | name = "serde_json" 361 | version = "1.0.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | dependencies = [ 364 | "dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", 365 | "itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 366 | "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 367 | "serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 368 | ] 369 | 370 | [[package]] 371 | name = "syn" 372 | version = "0.11.11" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | dependencies = [ 375 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 376 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 377 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 378 | ] 379 | 380 | [[package]] 381 | name = "synom" 382 | version = "0.11.3" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | dependencies = [ 385 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 386 | ] 387 | 388 | [[package]] 389 | name = "thread-id" 390 | version = "3.0.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | dependencies = [ 393 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 394 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 395 | ] 396 | 397 | [[package]] 398 | name = "thread_local" 399 | version = "0.3.3" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | dependencies = [ 402 | "thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 403 | "unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 404 | ] 405 | 406 | [[package]] 407 | name = "time" 408 | version = "0.1.37" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | dependencies = [ 411 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 412 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 413 | "redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", 414 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 415 | ] 416 | 417 | [[package]] 418 | name = "toml" 419 | version = "0.4.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | dependencies = [ 422 | "serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 423 | ] 424 | 425 | [[package]] 426 | name = "unicode-normalization" 427 | version = "0.1.4" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | 430 | [[package]] 431 | name = "unicode-xid" 432 | version = "0.0.4" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | 435 | [[package]] 436 | name = "unreachable" 437 | version = "0.1.1" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | dependencies = [ 440 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 441 | ] 442 | 443 | [[package]] 444 | name = "user32-sys" 445 | version = "0.2.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | dependencies = [ 448 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 449 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 450 | ] 451 | 452 | [[package]] 453 | name = "utf8-ranges" 454 | version = "1.0.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | 457 | [[package]] 458 | name = "void" 459 | version = "1.0.2" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | 462 | [[package]] 463 | name = "walkdir" 464 | version = "1.0.7" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | dependencies = [ 467 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 468 | "same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 469 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 470 | ] 471 | 472 | [[package]] 473 | name = "winapi" 474 | version = "0.2.8" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | 477 | [[package]] 478 | name = "winapi-build" 479 | version = "0.1.1" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | 482 | [metadata] 483 | "checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699" 484 | "checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4" 485 | "checksum bufstream 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f382711e76b9de6c744cc00d0497baba02fb00a787f088c879f01d09468e32" 486 | "checksum cargo_metadata 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5d84cb53c78e573aa126a4b9f963fdb2629f8183b26e235da08bb36dc7381162" 487 | "checksum clippy 0.0.133 (registry+https://github.com/rust-lang/crates.io-index)" = "9022a83e66a654b0fa11143d9e37ad65b03ec4e317ac7198bd59d4d8714e6d5f" 488 | "checksum clippy_lints 0.0.133 (registry+https://github.com/rust-lang/crates.io-index)" = "95e0860d69a264f5533a01e5efe83386e320d93db636f60e59a8a47dd4865868" 489 | "checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" 490 | "checksum env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e3856f1697098606fc6cb97a93de88ca3f3bc35bb878c725920e6e82ecf05e83" 491 | "checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d" 492 | "checksum gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "40899336fb50db0c78710f53e87afc54d8c7266fb76262fecc78ca1a7f09deae" 493 | "checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518" 494 | "checksum itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2f404fbc66fd9aac13e998248505e7ecb2ad8e44ab6388684c5fb11c6c251c" 495 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 496 | "checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf" 497 | "checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502" 498 | "checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" 499 | "checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1" 500 | "checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" 501 | "checksum nom 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5388009f277d5d889ee01f0ba92e3620c759402cdf52a8d40e9ac06969ee3c88" 502 | "checksum num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "98b15ba84e910ea7a1973bccd3df7b31ae282bf9d8bd2897779950c9b8303d40" 503 | "checksum num-bigint 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "ba6d838b16e56da1b6c383d065ff1ec3c7d7797f65a3e8f6ba7092fd87820bac" 504 | "checksum num-complex 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "148eb324ca772230853418731ffdf13531738b50f89b30692a01fcdcb0a64677" 505 | "checksum num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "ef1a4bf6f9174aa5783a9b4cc892cacd11aebad6c69ad027a0b65c6ca5f8aa37" 506 | "checksum num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d1891bd7b936f12349b7d1403761c8a0b85a18b148e9da4429d5d102c1a41e" 507 | "checksum num-rational 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "c2dc5ea04020a8f18318ae485c751f8cfa1c0e69dcf465c29ddaaa64a313cc44" 508 | "checksum num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "e1cbfa3781f3fe73dc05321bed52a06d2d491eaa764c52335cf4399f046ece99" 509 | "checksum openssl 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)" = "bb5d1663b73d10c6a3eda53e2e9d0346f822394e7b858d7257718f65f61dfbe2" 510 | "checksum openssl-sys 0.9.12 (registry+https://github.com/rust-lang/crates.io-index)" = "3a5886d87d3e2a0d890bf62dc8944f5e3769a405f7e1e9ef6e517e47fd7a0897" 511 | "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" 512 | "checksum quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "07589615d719a60c8dd8a4622e7946465dfef20d1a428f969e3443e7386d5f45" 513 | "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 514 | "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" 515 | "checksum redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "29dbdfd4b9df8ab31dec47c6087b7b13cbf4a776f335e4de8efba8288dda075b" 516 | "checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01" 517 | "checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457" 518 | "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" 519 | "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 520 | "checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7" 521 | "checksum semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a3186ec9e65071a2095434b1f5bb24838d4e8e130f584c790f6033c79943537" 522 | "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 523 | "checksum serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3b46a59dd63931010fdb1d88538513f3279090d88b5c22ef4fe8440cfffcc6e3" 524 | "checksum serde_derive 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6c06b68790963518008b8ae0152d48be4bbbe77015d2c717f6282eea1824be9a" 525 | "checksum serde_derive_internals 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "021c338d22c7e30f957a6ab7e388cb6098499dda9fd4ba1661ee074ca7a180d1" 526 | "checksum serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "48b04779552e92037212c3615370f6bd57a40ebba7f20e554ff9f55e41a69a7b" 527 | "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 528 | "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 529 | "checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a" 530 | "checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7" 531 | "checksum time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "ffd7ccbf969a892bf83f1e441126968a07a3941c24ff522a26af9f9f4585d1a3" 532 | "checksum toml 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3063405db158de3dce8efad5fc89cf1baffb9501a3647dc9505ba109694ce31f" 533 | "checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff" 534 | "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 535 | "checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91" 536 | "checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" 537 | "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" 538 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 539 | "checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff" 540 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 541 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 542 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "segimap" 3 | version = "0.0.1" 4 | authors = [] 5 | 6 | [[bin]] 7 | 8 | name = "segimap" 9 | path = "src/main.rs" 10 | 11 | [dependencies] 12 | clippy = { version = "0.0", optional = true } 13 | bufstream = "*" 14 | env_logger = "*" 15 | log = "*" 16 | nom = "*" 17 | num = "*" 18 | openssl = "*" 19 | rand = "*" 20 | regex = "*" 21 | rust-crypto = "*" 22 | segimap_mime = { path = "../mime/" } 23 | serde = "*" 24 | serde_derive = "*" 25 | serde_json = "*" 26 | time = "*" 27 | toml = "*" 28 | walkdir = "*" 29 | 30 | [features] 31 | unstable = [] 32 | nightly-testing = ["clippy", "unstable"] 33 | -------------------------------------------------------------------------------- /core/src/command/fetch.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use command::FetchCommand; 4 | use command::Attribute::BodySection; 5 | use folder::Folder; 6 | use parser::{self, ParserResult}; 7 | 8 | use message::Flag::Seen; 9 | use super::store::StoreName::Add; 10 | 11 | /// Take the rest of the arguments provided by the client and parse them into a 12 | /// `FetchCommand` object with `parser::fetch`. 13 | pub fn fetch(args: Vec<&str>) -> ParserResult { 14 | let mut cmd = "FETCH".to_string(); 15 | for arg in args { 16 | cmd.push(' '); 17 | cmd.push_str(arg); 18 | } 19 | 20 | parser::fetch(cmd.as_bytes()) 21 | } 22 | 23 | /// Perform the fetch operation on each sequence number indicated and return 24 | /// the response to be sent back to the client. 25 | pub fn fetch_loop(parsed_cmd: &FetchCommand, folder: &mut Folder, 26 | sequence_iter: &[usize], tag: &str, uid: bool) -> String { 27 | for attr in &parsed_cmd.attributes { 28 | if let BodySection(_, _) = *attr { 29 | let mut seen_flag_set = HashSet::new(); 30 | seen_flag_set.insert(Seen); 31 | folder.store(sequence_iter.to_vec(), &Add, true, seen_flag_set, 32 | false, tag); 33 | break; 34 | } 35 | } 36 | 37 | let mut res = String::new(); 38 | for i in sequence_iter { 39 | let index = if !uid { 40 | *i-1 41 | } else if let Some(index) = folder.get_index_from_uid(i) { 42 | *index 43 | } else { 44 | continue; 45 | }; 46 | res.push_str(&folder.fetch(index, &parsed_cmd.attributes)[..]); 47 | } 48 | res.push_str(tag); 49 | res.push_str(" OK "); 50 | if uid { 51 | res.push_str("UID "); 52 | } 53 | res.push_str("FETCH completed\r\n"); 54 | res 55 | } 56 | -------------------------------------------------------------------------------- /core/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sequence_set; 2 | pub mod store; 3 | pub mod fetch; 4 | 5 | use command::sequence_set::SequenceItem; 6 | 7 | use mime::BodySectionType; 8 | 9 | /// The different Attributes which a Fetch command may request. 10 | #[derive(PartialEq, Debug)] 11 | pub enum Attribute { 12 | Body, 13 | BodyPeek(BodySectionType, Option<(usize, usize)>), 14 | BodySection(BodySectionType, Option<(usize, usize)>), 15 | BodyStructure, 16 | Envelope, 17 | Flags, 18 | InternalDate, 19 | RFC822(RFC822Attribute), 20 | UID 21 | } 22 | 23 | /// Attributes defined as part of any electronic mail message 24 | #[derive(PartialEq, Debug)] 25 | pub enum RFC822Attribute { 26 | AllRFC822, 27 | HeaderRFC822, 28 | SizeRFC822, 29 | TextRFC822 30 | } 31 | 32 | /// This represents a Fetch command; 33 | /// It has a list of message ids (either UIDs or indexes into the folder's list 34 | /// of messages) 35 | /// It has a list of message attributes which are being requested. 36 | #[derive(PartialEq, Debug)] 37 | pub struct FetchCommand { 38 | pub sequence_set: Vec, 39 | pub attributes: Vec 40 | } 41 | 42 | impl FetchCommand { 43 | pub fn new(sequence_set: Vec, attributes: Vec) 44 | -> FetchCommand { 45 | FetchCommand { 46 | sequence_set: sequence_set, 47 | attributes: attributes 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/command/sequence_set.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Iterator; 2 | 3 | use self::SequenceItem::{Number, Range, Wildcard}; 4 | 5 | /// This represents an individual item in the list of requested message ids 6 | /// passed by the client 7 | /// The client can pass a number representing a single id, a wildcard 8 | /// (represented by *) or a range which is made up of a start non-range sequence 9 | /// item and an end non-range sequence item separated by a colon 10 | /// A range represents all items with ids between its start and end, inclusive. 11 | #[derive(Clone, PartialEq, Debug)] 12 | pub enum SequenceItem { 13 | Number(usize), 14 | Range(Box, Box), 15 | Wildcard 16 | } 17 | 18 | fn parse_item(item: &str) -> Option { 19 | if let Ok(intseq) = item.parse() { 20 | Some(Number(intseq)) 21 | } else if item == "*" { 22 | // item is not a valid number 23 | // If it is the wildcard value return that 24 | // Otherwise, return no sequence item 25 | Some(Wildcard) 26 | } else { 27 | None 28 | } 29 | } 30 | 31 | /// Given a string as passed in from the client, create a list of sequence items 32 | /// If the string does not represent a valid list, return None 33 | pub fn parse(sequence_string: &str) -> Option> { 34 | let sequences = sequence_string.split(','); 35 | let mut sequence_set = Vec::new(); 36 | for sequence in sequences { 37 | let mut range = sequence.split(':'); 38 | let start = match range.next() { 39 | None => return None, 40 | Some(seq) => { 41 | match parse_item(seq) { 42 | None => return None, 43 | Some(item) => item 44 | } 45 | } 46 | }; 47 | let stop = match range.next() { 48 | None => { 49 | // Nothing after ':' 50 | // Add the number or wildcard and bail out 51 | sequence_set.push(start); 52 | continue; 53 | } 54 | Some(seq) => { 55 | match parse_item(seq) { 56 | None => return None, 57 | Some(item) => item 58 | } 59 | } 60 | }; 61 | 62 | // A valid range only has one colon 63 | if range.next().is_some() { 64 | return None; 65 | } 66 | 67 | sequence_set.push(Range(Box::new(start), Box::new(stop))); 68 | } 69 | Some(sequence_set) 70 | } 71 | 72 | /// Create the list of unsigned integers representing valid ids from a list of 73 | /// sequence items. Ideally this would handle wildcards in O(1) rather than O(n) 74 | pub fn iterator(sequence_set: &[SequenceItem], max_id: usize) -> Vec { 75 | // If the number of possible messages is 0, we return an empty vec. 76 | if max_id == 0 { return Vec::new() } 77 | 78 | let stop = max_id + 1; 79 | 80 | let mut items = Vec::new(); 81 | for item in sequence_set.iter() { 82 | match *item { 83 | // For a number we just need the value 84 | Number(num) => { items.push(num) }, 85 | // For a range we need all the values inside it 86 | Range(ref a, ref b) => { 87 | // Grab the start of the range 88 | let a = match **a { 89 | Number(num) => { num }, 90 | Wildcard => { max_id } 91 | Range(_, _) => { 92 | error!("A range of ranges is invalid."); 93 | continue; 94 | } 95 | }; 96 | // Grab the end of the range 97 | let b = match **b { 98 | Number(num) => { num }, 99 | Wildcard => { max_id } 100 | Range(_, _) => { 101 | error!("A range of ranges is invalid."); 102 | continue; 103 | } 104 | }; 105 | // Figure out which way the range points 106 | let (mut min, mut max) = if a <= b { 107 | (a, b) 108 | } else { 109 | (b, a) 110 | }; 111 | 112 | // Bounds checks 113 | if min > stop { min = stop; } 114 | if max > stop { max = stop; } 115 | 116 | // Generate the list of values between the min and max 117 | let seq_range: Vec = (min..max + 1).collect(); 118 | items.extend(seq_range.iter()); 119 | }, 120 | Wildcard => { 121 | // If the sequence set contains the wildcard operator, it spans 122 | // the entire possible range of messages. 123 | return (1..stop).collect() 124 | } 125 | } 126 | } 127 | 128 | // Sort and remove duplicates. 129 | items.sort(); 130 | items.dedup(); 131 | // Remove all elements that are greater than the maximum. 132 | let items: Vec = items.into_iter().filter(|&x| x <= max_id).collect(); 133 | items 134 | } 135 | 136 | pub fn uid_iterator(sequence_set: &[SequenceItem]) -> Vec { 137 | let mut items = Vec::new(); 138 | for item in sequence_set.iter() { 139 | match *item { 140 | Number(num) => { items.push(num) }, 141 | Range(ref a, ref b) => { 142 | let a = match **a { 143 | Number(num) => { num }, 144 | Wildcard => { return Vec::new() } 145 | Range(_, _) => { 146 | error!("A range of ranges is invalid."); 147 | continue; 148 | } 149 | }; 150 | let b = match **b { 151 | Number(num) => { num }, 152 | Wildcard => { return Vec::new() } 153 | Range(_, _) => { 154 | error!("A range of ranges is invalid."); 155 | continue; 156 | } 157 | }; 158 | let (min, max) = if a <= b { 159 | (a, b) 160 | } else { 161 | (b, a) 162 | }; 163 | //if min > stop { min = stop; } 164 | //if max > stop { max = stop; } 165 | let seq_range: Vec = (min..max + 1).collect(); 166 | items.extend(seq_range.iter()); 167 | }, 168 | Wildcard => { 169 | return Vec::new() 170 | } 171 | } 172 | } 173 | 174 | // Sort and remove duplicates. 175 | items.sort(); 176 | items.dedup(); 177 | // Remove all elements that are greater than the maximum. 178 | //let items: Vec = items.into_iter().filter(|&x| x <= max_id).collect(); 179 | items 180 | } 181 | 182 | #[test] 183 | fn test_sequence_num() { 184 | assert_eq!(iterator(&[Number(4324)], 5000), vec![4324]); 185 | assert_eq!(iterator(&[Number(23), Number(44)], 5000), vec![23, 44]); 186 | assert_eq!(iterator(&[Number(6), Number(6), Number(2)], 5000), vec![2, 6]); 187 | } 188 | 189 | #[test] 190 | fn test_sequence_past_end() { 191 | // Needed to help rust infer the type for the empty vec. 192 | let empty_vec: Vec = Vec::new(); 193 | 194 | assert_eq!(iterator(&[Number(4324)], 100), empty_vec); 195 | assert_eq!(iterator(&[Number(23), Number(44)], 30), vec![23]); 196 | assert_eq!(iterator(&[Number(6), Number(6), Number(2)], 4), vec![2]); 197 | } 198 | 199 | #[test] 200 | fn test_sequence_range() { 201 | assert_eq!(iterator(&[Range(Box::new(Number(6)), Box::new(Wildcard))], 10), vec![6, 7, 8, 9, 10]); 202 | assert_eq!(iterator(&[Range(Box::new(Number(1)), Box::new(Number(10)))], 4), vec![1, 2, 3, 4]); 203 | assert_eq!(iterator(&[Range(Box::new(Wildcard), Box::new(Number(8))), Number(9), Number(2), Number(2)], 12), vec![2, 8, 9, 10, 11, 12]); 204 | } 205 | 206 | #[test] 207 | fn test_sequence_wildcard() { 208 | assert_eq!(iterator(&[Range(Box::new(Number(10)), Box::new(Wildcard)), Wildcard], 6), vec![1, 2, 3, 4, 5, 6]); 209 | assert_eq!(iterator(&[Wildcard, Number(8)], 3), vec![1, 2, 3]); 210 | } 211 | 212 | #[test] 213 | fn test_sequence_complex() { 214 | assert_eq!(iterator( 215 | &[Number(1), Number(3), Range(Box::new(Number(5)), Box::new(Number(7))), Number(9), Number(12), Range(Box::new(Number(15)), Box::new(Wildcard))], 13), 216 | vec![1, 3, 5, 6, 7, 9, 12, 13]); 217 | } 218 | -------------------------------------------------------------------------------- /core/src/command/store.rs: -------------------------------------------------------------------------------- 1 | use std::ascii::AsciiExt; 2 | use std::collections::HashSet; 3 | 4 | use folder::Folder; 5 | use message::Flag; 6 | use message::parse_flag; 7 | 8 | use self::StoreName::{Add, Replace, Sub}; 9 | use super::sequence_set; 10 | 11 | /// Representation of a STORE operation 12 | pub enum StoreName { 13 | Replace, // replace current flags with new flags 14 | Add, // add new flags to current flags 15 | Sub // remove new flags from current flags 16 | } 17 | 18 | /// Parse and perform the store operation specified by `store_args`. Returns the 19 | /// response to the client or None if a BAD response should be sent back to 20 | /// the client 21 | pub fn store(folder: &mut Folder, store_args: &[&str], seq_uid: bool, 22 | tag: &str) -> Option { 23 | if store_args.len() < 3 { return None; } 24 | 25 | // Parse the sequence set argument 26 | let sequence_set_opt = sequence_set::parse(store_args[0].trim_matches('"')); 27 | // Grab how to handle the flags. It should be case insensitive. 28 | let data_name = store_args[1].trim_matches('"').to_ascii_lowercase(); 29 | 30 | // Split into "flag" part and "silent" part. 31 | let mut data_name_parts = (&data_name[..]).split('.'); 32 | let flag_part = data_name_parts.next(); 33 | let silent_part = data_name_parts.next(); 34 | 35 | // There shouldn't be any more parts to the data name argument 36 | if data_name_parts.next().is_some() { 37 | return None; 38 | } 39 | 40 | // Grab the flags themselves. 41 | let data_value = store_args[2].trim_matches('"'); 42 | 43 | // Set the silent flag if it is present. If there is something else 44 | // instead of the word "silent", a BAD response should be sent to the 45 | // client. 46 | let silent = match silent_part { 47 | None => false, 48 | Some(part) => { 49 | if part == "silent" { 50 | true 51 | } else { 52 | return None 53 | } 54 | } 55 | }; 56 | 57 | // Parse the flag_part into an enum describing what to do with the 58 | // data_value. 59 | let flag_name = match parse_storename(flag_part) { 60 | Some(storename) => storename, 61 | None => return None 62 | }; 63 | 64 | // Create the Set of flags to be STORE'd from the data_value argument. 65 | let mut flags: HashSet = HashSet::new(); 66 | for flag in data_value.trim_matches('(').trim_matches(')').split(' ') { 67 | match parse_flag(flag) { 68 | None => { continue; } 69 | Some(insert_flag) => { flags.insert(insert_flag); } 70 | } 71 | } 72 | 73 | // Perform the STORE operation on each message specified by the 74 | // sequence set. 75 | match sequence_set_opt { 76 | None => None, 77 | Some(sequence_set) => { 78 | let sequence_iter = if seq_uid { 79 | sequence_set::uid_iterator(&sequence_set) 80 | } else { 81 | sequence_set::iterator(&sequence_set, folder.message_count()) 82 | }; 83 | let res = folder.store(sequence_iter, &flag_name, silent, flags, 84 | seq_uid, tag); 85 | Some(res) 86 | } 87 | } 88 | } 89 | 90 | /// Takes the argument specifying what to do with the provided flags in a store 91 | /// operation and returns the corresponding enum. 92 | fn parse_storename(storename: Option<&str>) -> Option { 93 | match storename { 94 | Some("flags") => Some(Replace), 95 | Some("+flags") => Some(Add), 96 | Some("-flags") => Some(Sub), 97 | _ => None 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src/error.rs: -------------------------------------------------------------------------------- 1 | use mime; 2 | use serde_json::Error as JsonError; 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | use std::io; 6 | use std::result::Result as StdResult; 7 | use toml::ser::Error as TomlError; 8 | 9 | /// A convenient alias type for results for `segimap`. 10 | pub type ImapResult = StdResult; 11 | 12 | /// Represents errors which occur during SEGIMAP operation. 13 | #[derive(Debug)] 14 | pub enum Error { 15 | InvalidImapState, 16 | /// An internal `std::io` error. 17 | Io(io::Error), 18 | /// An internal `serde_json` error which occurs when serializing or 19 | /// deserializing JSON data. 20 | Json(JsonError), 21 | /// An error which occurs when attempting to read the UID for a message. 22 | MessageUidDecode, 23 | /// An error which occurs when a Maildir message has a bad filename 24 | MessageBadFilename, 25 | /// An internal `mime` error. 26 | Mime(mime::Error), 27 | /// An internal `toml` error which occurs when serializing or deserializing 28 | /// TOML data. 29 | Toml(TomlError), 30 | } 31 | 32 | impl fmt::Display for Error { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 34 | use self::Error::*; 35 | 36 | match *self { 37 | InvalidImapState | MessageUidDecode | MessageBadFilename => write!(f, "{}", StdError::description(self)), 38 | Io(ref e) => e.fmt(f), 39 | Json(ref e) => e.fmt(f), 40 | Mime(ref e) => e.fmt(f), 41 | Toml(ref e) => e.fmt(f), 42 | } 43 | } 44 | } 45 | 46 | impl StdError for Error { 47 | fn description(&self) -> &str { 48 | use self::Error::*; 49 | 50 | match *self { 51 | InvalidImapState => "Not in selected state.", 52 | MessageUidDecode => "An error occured while decoding the UID for a message.", 53 | MessageBadFilename => "An error occured while parsing message information from its filename", 54 | Io(ref e) => e.description(), 55 | Json(ref e) => e.description(), 56 | Mime(ref e) => e.description(), 57 | Toml(ref e) => e.description(), 58 | } 59 | } 60 | 61 | fn cause(&self) -> Option<&StdError> { 62 | use self::Error::*; 63 | 64 | match *self { 65 | InvalidImapState | MessageUidDecode | MessageBadFilename => None, 66 | Io(ref e) => e.cause(), 67 | Json(ref e) => e.cause(), 68 | Mime(ref e) => e.cause(), 69 | Toml(ref e) => e.cause(), 70 | } 71 | } 72 | } 73 | 74 | // Implement `PartialEq` manually, since `std::io::Error` does not implement it. 75 | impl PartialEq for Error { 76 | fn eq(&self, other: &Error) -> bool { 77 | use self::Error::*; 78 | 79 | match (self, other) { 80 | (&InvalidImapState, &InvalidImapState) | 81 | (&Io(_), &Io(_)) | 82 | (&Json(_), &Json(_)) | 83 | (&Mime(_), &Mime(_)) | 84 | (&Toml(_), &Toml(_)) => true, 85 | _ => false, 86 | } 87 | } 88 | } 89 | 90 | impl From for Error { 91 | fn from(error: io::Error) -> Error { 92 | Error::Io(error) 93 | } 94 | } 95 | 96 | impl From for Error { 97 | fn from(error: JsonError) -> Error { 98 | Error::Json(error) 99 | } 100 | } 101 | 102 | impl From for Error { 103 | fn from(error: mime::Error) -> Error { 104 | Error::Mime(error) 105 | } 106 | } 107 | 108 | impl From for Error { 109 | fn from(error: TomlError) -> Error { 110 | Error::Toml(error) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/folder.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap,HashSet}; 2 | use std::fs; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use std::path::PathBuf; 6 | 7 | use command::Attribute; 8 | use message::Message; 9 | use message::Flag; 10 | 11 | use command::store::StoreName; 12 | 13 | /// Representation of a Folder 14 | #[derive(Clone, Debug)] 15 | pub struct Folder { 16 | // How many messages are in folder/new/ 17 | recent: usize, 18 | // How many messages are in the folder total 19 | exists: usize, 20 | // How many messages are not marked with the Seen flag 21 | unseen: usize, 22 | // Whether the folder has been opened as read-only or not 23 | readonly: bool, 24 | path: PathBuf, 25 | messages: Vec, 26 | // A mapping of message uids to indices in folder.messages 27 | uid_to_seqnum: HashMap 28 | } 29 | 30 | // Macro to handle each message in the folder 31 | macro_rules! handle_message( 32 | ($msg_path_entry:ident, $uid_map:ident, $messages:ident, $i:ident, $unseen:ident) => ({ 33 | if let Ok(msg_path) = $msg_path_entry { 34 | if let Ok(message) = Message::new(msg_path.path().as_path()) { 35 | if $unseen == !0usize && message.is_unseen() { 36 | $unseen = $i; 37 | } 38 | $uid_map.insert(message.get_uid(), $i); 39 | $i += 1; 40 | $messages.push(message); 41 | } 42 | } 43 | }); 44 | ); 45 | 46 | // Perform a rename operation on a message 47 | macro_rules! rename_message( 48 | ($msg:ident, $curpath:expr, $new_messages:ident) => ({ 49 | if fs::rename($msg.get_path(), &$curpath).is_ok() { 50 | // if the rename operation succeeded then clone the message, 51 | // update its path and add the clone to our new list 52 | $new_messages.push($msg.rename($curpath)); 53 | } else { 54 | // if the rename failed, just add the old message to our 55 | // new list 56 | $new_messages.push($msg.clone()); 57 | } 58 | }) 59 | ); 60 | 61 | impl Folder { 62 | pub fn new(path: PathBuf, examine: bool) -> Option { 63 | // the EXAMINE command is always read-only or we test SELECT for read-only status 64 | // We use a lock file to determine write access on a folder 65 | let readonly = if examine || fs::File::open(&path.join(".lock")).is_ok() { 66 | true 67 | } else { 68 | if let Ok(mut file) = fs::File::create(&path.join(".lock")) { 69 | // Get the compiler to STFU with this match 70 | let _ = file.write(b"selected"); 71 | false 72 | } else { 73 | true 74 | } 75 | }; 76 | 77 | if let Ok(cur) = fs::read_dir(&(path.join("cur"))) { 78 | if let Ok(new) = fs::read_dir(&(path.join("new"))) { 79 | let mut messages = Vec::new(); 80 | let mut uid_to_seqnum: HashMap = HashMap::new(); 81 | let mut i = 0usize; 82 | let mut unseen = !0usize; 83 | 84 | // populate messages 85 | for msg_path in cur { 86 | handle_message!(msg_path, uid_to_seqnum, messages, i, unseen); 87 | } 88 | 89 | let old = i; 90 | for msg_path in new { 91 | handle_message!(msg_path, uid_to_seqnum, messages, i, unseen); 92 | } 93 | 94 | // Move the messages from folder/new to folder/cur 95 | messages = move_new(&messages, path.as_path(), unseen); 96 | return Some(Folder { 97 | path: path, 98 | recent: i-old, 99 | unseen: unseen, 100 | exists: i, 101 | messages: messages, 102 | readonly: readonly, 103 | uid_to_seqnum: uid_to_seqnum, 104 | }); 105 | } 106 | } 107 | None 108 | } 109 | 110 | /// Generate the SELECT/EXAMINE response based on data in the folder 111 | pub fn select_response(&self, tag: &str) -> String { 112 | let unseen_res = if self.unseen <= self.exists { 113 | let unseen_str = self.unseen.to_string(); 114 | let mut res = "* OK [UNSEEN ".to_string(); 115 | res.push_str(&unseen_str[..]); 116 | res.push_str("] Message "); 117 | res.push_str(&unseen_str[..]); 118 | res.push_str("th is the first unseen\r\n"); 119 | res 120 | } else { 121 | "".to_string() 122 | }; 123 | 124 | let read_status = if self.readonly { 125 | "[READ-ONLY]" 126 | } else { 127 | "[READ-WRITE]" 128 | }; 129 | 130 | // * EXISTS 131 | // * RECENT 132 | // * OK UNSEEN 133 | // * Flags - Should match values in enum Flag in message.rs 134 | // * OK PERMANENTFLAG - Should match values in enum Flag in message.rs 135 | // * OK UIDNEXT 136 | // * OK UIDVALIDITY 137 | format!("* {} EXISTS\r\n* {} RECENT\r\n{}* FLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen)\r\n* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen)] Permanent flags\r\n{} OK {} SELECT command was successful\r\n", 138 | self.exists, self.recent, unseen_res, tag, read_status) 139 | } 140 | 141 | /// Delete on disk all the messages marked for deletion 142 | /// Returns the list of sequence numbers which have been deleted on disk 143 | /// Per RFC 3501, the later sequence numbers are calculated based on the 144 | /// sequence numbers at the time of the deletion not at the start of the function 145 | pub fn expunge(&self) -> Vec { 146 | let mut result = Vec::new(); 147 | // We can't perform the deletion if the folder has been opened as 148 | // read-only 149 | if !self.readonly { 150 | // Vectors are 0-indexed 151 | let mut index = 0usize; 152 | 153 | // self.messages will get smaller as we go through it 154 | while index < self.messages.len() { 155 | if self.messages[index].remove_if_deleted() { 156 | // Sequence numbers are 1-indexed 157 | result.push(index + 1); 158 | } else { 159 | index += 1; 160 | } 161 | } 162 | // Get the compiler to STFU with empty match block 163 | match fs::remove_file(&self.path.join(".lock")) { _ => {} } 164 | } 165 | result 166 | } 167 | 168 | pub fn message_count(&self) -> usize { 169 | self.messages.len() 170 | } 171 | 172 | /// Perform a fetch of the specified attributes on self.messsages[index] 173 | /// Return the FETCH response string to be sent back to the client 174 | pub fn fetch(&self, index: usize, attributes: &[Attribute]) -> String { 175 | let mut res = "* ".to_string(); 176 | res.push_str(&(index+1).to_string()[..]); 177 | res.push_str(" FETCH ("); 178 | res.push_str(&self.messages[index].fetch(attributes)[..]); 179 | res.push_str(")\r\n"); 180 | res 181 | } 182 | 183 | /// Turn a UID into a sequence number 184 | pub fn get_index_from_uid(&self, uid: &usize) -> Option<&usize> { 185 | self.uid_to_seqnum.get(uid) 186 | } 187 | 188 | /// Perform a STORE on the specified set of sequence numbers 189 | /// This modifies the flags of the specified messages 190 | /// Returns the String response to be sent back to the client. 191 | pub fn store(&mut self, sequence_set: Vec, flag_name: &StoreName, 192 | silent: bool, flags: HashSet, seq_uid: bool, 193 | tag: &str) -> String { 194 | let mut responses = String::new(); 195 | for num in &sequence_set { 196 | let (uid, i) = if seq_uid { 197 | match self.get_index_from_uid(num) { 198 | // 0 is an invalid sequence number 199 | // Return it if the UID isn't found 200 | None => (*num, 0usize), 201 | Some(ind) => (*num, *ind+1) 202 | } 203 | } else { 204 | (0usize, *num) 205 | }; 206 | 207 | // if i == 0 then the UID wasn't in the sequence number map 208 | if i == 0 { 209 | continue; 210 | } 211 | 212 | // Create the FETCH response for this STORE operation. 213 | if let Some(mut message) = self.messages.get_mut(i-1) { 214 | responses.push_str("* "); 215 | responses.push_str(&i.to_string()[..]); 216 | responses.push_str(" FETCH (FLAGS "); 217 | responses.push_str(&message.store(flag_name, flags.clone())[..]); 218 | 219 | // UID STORE needs to respond with the UID for each FETCH response 220 | if seq_uid { 221 | let uid_res = format!(" UID {}", uid); 222 | responses.push_str(&uid_res[..]); 223 | } 224 | responses.push_str(" )\r\n"); 225 | } 226 | } 227 | 228 | // Return an empty string if the client wanted the STORE to be SILENT 229 | if silent { 230 | responses = String::new(); 231 | } 232 | responses.push_str(tag); 233 | responses.push_str(" OK STORE complete\r\n"); 234 | responses 235 | } 236 | 237 | /// Reconcile the internal state of the folder with the disk. 238 | pub fn check(&mut self) { 239 | // If it is read-only we can't write any changes to disk 240 | if self.readonly { 241 | return; 242 | } 243 | 244 | // We need to create a new list of messages because the compiler will 245 | // yell at us for inspecting the internal state of the message and 246 | // modifying that state at the same time 247 | let mut new_messages = Vec::new(); 248 | for msg in &self.messages { 249 | // Grab the new filename composed of this message's UID and its current flags. 250 | let filename = msg.get_new_filename(); 251 | let curpath = self.path.join("cur").join(filename); 252 | 253 | // If the new filename is the same as the current filename, add the 254 | // current message to our new list and move on to the next message 255 | if curpath == msg.get_path() { 256 | new_messages.push(msg.clone()); 257 | continue; 258 | } 259 | rename_message!(msg, curpath, new_messages); 260 | } 261 | 262 | // Set the current list of messages to the new list of messages 263 | // The compiler *should* make this discard the old list... 264 | self.messages = new_messages; 265 | } 266 | } 267 | 268 | /// This moves a list of messages from folder/new/ to folder/cur/ and returns a 269 | /// new list of messages 270 | fn move_new(messages: &[Message], path: &Path, 271 | start_index: usize) -> Vec { 272 | let mut new_messages = Vec::new(); 273 | 274 | // Go over the messages by index 275 | for (i, msg) in messages.iter().enumerate() { 276 | // messages before start_index are already in folder/cur/ 277 | if i+1 < start_index { 278 | new_messages.push(msg.clone()); 279 | continue; 280 | } 281 | let curpath = path.join("cur").join(msg.get_uid().to_string()); 282 | rename_message!(msg, curpath, new_messages); 283 | } 284 | 285 | // Return the new list of messages 286 | new_messages 287 | } 288 | -------------------------------------------------------------------------------- /core/src/main.rs: -------------------------------------------------------------------------------- 1 | //! SEGIMAP is an IMAP server implementation. 2 | #![deny(non_camel_case_types)] 3 | #![cfg_attr(feature = "unstable", feature(test))] 4 | #![cfg_attr(feature = "clippy", feature(plugin))] 5 | #![cfg_attr(feature = "clippy", plugin(clippy))] 6 | 7 | extern crate bufstream; 8 | extern crate crypto; 9 | extern crate env_logger; 10 | #[macro_use] 11 | extern crate log; 12 | extern crate mime; 13 | #[macro_use] 14 | extern crate nom; 15 | extern crate num; 16 | extern crate openssl; 17 | extern crate rand; 18 | extern crate regex; 19 | extern crate serde; 20 | #[macro_use] 21 | extern crate serde_derive; 22 | extern crate serde_json; 23 | extern crate time; 24 | extern crate toml; 25 | extern crate walkdir; 26 | 27 | use server::{lmtp_serve, imap_serve, Server}; 28 | 29 | use std::net::{TcpListener, TcpStream}; 30 | use std::sync::Arc; 31 | use std::thread::spawn; 32 | 33 | mod command; 34 | mod error; 35 | mod folder; 36 | mod parser; 37 | #[macro_use] 38 | mod util; 39 | #[macro_use] 40 | mod server; 41 | mod message; 42 | 43 | fn listen_generic(v: TcpListener, serv: Arc, prot: &str, serve_func: (fn(Arc, TcpStream))) { 44 | for stream in v.incoming() { 45 | match stream { 46 | Err(e) => { 47 | error!("Error accepting incoming {} connection: {}", prot, e); 48 | } 49 | Ok(stream) => { 50 | let session_serv = serv.clone(); 51 | spawn(move || { serve_func(session_serv, stream) }); 52 | } 53 | } 54 | } 55 | } 56 | 57 | fn listen_lmtp(v: TcpListener, serv: Arc) { 58 | listen_generic(v, serv, "LMTP", lmtp_serve); 59 | } 60 | 61 | fn listen_imap(v: TcpListener, serv: Arc) { 62 | listen_generic(v, serv, "IMAP", imap_serve); 63 | } 64 | 65 | fn main() { 66 | let _ = env_logger::init().unwrap(); 67 | info!("Application started"); 68 | 69 | // Create the server. We wrap it so that it is atomically reference 70 | // counted. This allows us to safely share it across threads 71 | 72 | let serv = match Server::new() { 73 | Err(e) => { 74 | error!("Error starting server: {}", e); 75 | return; 76 | }, 77 | Ok(s) => Arc::new(s) 78 | }; 79 | 80 | // Spawn a separate thread for listening for LMTP connections 81 | let lmtp_h = if let Some(lmtp_listener) = serv.lmtp_listener() { 82 | match lmtp_listener { 83 | Err(e) => { 84 | error!("Error listening on LMTP port: {}", e); 85 | None 86 | } 87 | Ok(v) => { 88 | let lmtp_serv = serv.clone(); 89 | Some(spawn(move || listen_lmtp(v, lmtp_serv))) 90 | } 91 | } 92 | } else { None }; 93 | 94 | let lmtp_ssl_h = if let Some(lmtp_listener) = serv.lmtp_ssl_listener() { 95 | match lmtp_listener { 96 | Err(e) => { 97 | error!("Error listening on LMTP SSL port: {}", e); 98 | None 99 | } 100 | Ok(v) => { 101 | let lmtp_serv = serv.clone(); 102 | Some(spawn(move || listen_lmtp(v, lmtp_serv))) 103 | } 104 | } 105 | } else { None }; 106 | 107 | // The main thread handles listening for IMAP connections 108 | let imap_h = if let Some(imap_listener) = serv.imap_listener() { 109 | match imap_listener { 110 | Err(e) => { 111 | error!("Error listening on IMAP port: {}", e); 112 | None 113 | } 114 | Ok(v) => { 115 | let imap_serv = serv.clone(); 116 | Some(spawn(move || listen_imap(v, imap_serv))) 117 | } 118 | } 119 | } else { None }; 120 | 121 | let imap_ssl_h = if let Some(imap_listener) = serv.imap_ssl_listener() { 122 | match imap_listener { 123 | Err(e) => { 124 | error!("Error listening on IMAP port: {}", e); 125 | None 126 | } 127 | Ok(v) => { 128 | Some(spawn(move || listen_imap(v, serv))) 129 | } 130 | } 131 | } else { None }; 132 | 133 | if let Some(lh) = lmtp_h { 134 | return_on_err!(lh.join()); 135 | } 136 | 137 | if let Some(lsh) = lmtp_ssl_h { 138 | return_on_err!(lsh.join()); 139 | } 140 | 141 | if let Some(ih) = imap_h { 142 | return_on_err!(ih.join()); 143 | } 144 | 145 | if let Some(ish) = imap_ssl_h { 146 | return_on_err!(ish.join()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /core/src/message.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::fs; 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | use std::str; 6 | 7 | use command::Attribute; 8 | use command::Attribute::{ 9 | Envelope, 10 | Flags, 11 | InternalDate, 12 | RFC822, 13 | Body, 14 | BodyPeek, 15 | BodySection, 16 | BodyStructure, 17 | UID 18 | }; 19 | use command::RFC822Attribute::{ 20 | AllRFC822, 21 | HeaderRFC822, 22 | SizeRFC822, 23 | TextRFC822 24 | }; 25 | use command::store::StoreName; 26 | 27 | use error::{Error, ImapResult}; 28 | 29 | use mime::Message as MIME_Message; 30 | 31 | use time; 32 | use time::Timespec; 33 | 34 | /// Representation of a message flag 35 | #[derive(Eq, PartialEq, Hash, Debug, Clone)] 36 | pub enum Flag { 37 | Answered, 38 | Draft, 39 | Flagged, 40 | Seen, 41 | Deleted 42 | } 43 | 44 | /// Takes a flag argument and returns the corresponding enum. 45 | pub fn parse_flag(flag: &str) -> Option { 46 | match flag { 47 | "\\Deleted" => Some(Flag::Deleted), 48 | "\\Seen" => Some(Flag::Seen), 49 | "\\Draft" => Some(Flag::Draft), 50 | "\\Answered" => Some(Flag::Answered), 51 | "\\Flagged" => Some(Flag::Flagged), 52 | _ => None 53 | } 54 | } 55 | 56 | /// Representation of a Message 57 | #[derive(Debug, Clone)] 58 | pub struct Message { 59 | // a unique id (timestamp) for the message 60 | uid: usize, 61 | 62 | // filename 63 | path: PathBuf, 64 | 65 | mime_message: MIME_Message, 66 | 67 | // contains the message's flags 68 | flags: HashSet, 69 | 70 | // marks the message for deletion 71 | deleted: bool, 72 | 73 | } 74 | 75 | impl Message { 76 | pub fn new(arg_path: &Path) -> ImapResult { 77 | let mime_message = MIME_Message::new(arg_path)?; 78 | 79 | // Grab the string in the filename representing the flags 80 | let mut path = path_filename_to_str!(arg_path).splitn(2, ':'); 81 | let filename = match path.next() { 82 | Some(fname) => fname, 83 | None => { return Err(Error::MessageBadFilename); } 84 | }; 85 | let path_flags = path.next(); 86 | 87 | // Retrieve the UID from the provided filename. 88 | let uid = filename.parse().map_err(|_| Error::MessageUidDecode)?; 89 | 90 | // Parse the flags from the filename. 91 | let flags = match path_flags { 92 | // if there are no flags, create an empty set 93 | None => HashSet::new(), 94 | Some(flags) => 95 | // The uid is separated from the flag part of the filename by a 96 | // colon. The flag part consists of a 2 followed by a comma and 97 | // then some letters. Those letters represent the message flags 98 | match flags.splitn(2, ',').nth(1) { 99 | None => HashSet::new(), 100 | Some(unparsed_flags) => { 101 | let mut set_flags: HashSet = HashSet::new(); 102 | for flag in unparsed_flags.chars() { 103 | let parsed_flag = match flag { 104 | 'D' => Some(Flag::Draft), 105 | 'F' => Some(Flag::Flagged), 106 | 'R' => Some(Flag::Answered), 107 | 'S' => Some(Flag::Seen), 108 | _ => None 109 | }; 110 | if let Some(enum_flag) = parsed_flag { 111 | set_flags.insert(enum_flag); 112 | } 113 | } 114 | set_flags 115 | } 116 | } 117 | }; 118 | 119 | let message = Message { 120 | uid: uid, 121 | path: arg_path.to_path_buf(), 122 | mime_message: mime_message, 123 | flags: flags, 124 | deleted: false 125 | }; 126 | 127 | Ok(message) 128 | } 129 | 130 | /// convenience method for determining if Seen is in this message's flags 131 | pub fn is_unseen(&self) -> bool { 132 | self.flags.contains(&Flag::Seen) 133 | } 134 | 135 | pub fn rename(&self, pb: PathBuf) -> Message { 136 | Message { 137 | uid: self.uid, 138 | path: pb, 139 | mime_message: self.mime_message.clone(), 140 | flags: self.flags.clone(), 141 | deleted: self.deleted 142 | } 143 | } 144 | 145 | pub fn remove_if_deleted(&self) -> bool { 146 | if self.deleted { 147 | // Get the compiler to STFU with empty match block 148 | match fs::remove_file(self.path.as_path()) { 149 | _ => {} 150 | } 151 | } 152 | self.deleted 153 | } 154 | 155 | pub fn get_path(&self) -> &Path { 156 | self.path.as_path() 157 | } 158 | 159 | pub fn get_uid(&self) -> usize { 160 | self.uid 161 | } 162 | 163 | pub fn store(&mut self, flag_name: &StoreName, 164 | new_flags: HashSet) -> String { 165 | match *flag_name { 166 | StoreName::Sub => { 167 | for flag in &new_flags { 168 | self.flags.remove(flag); 169 | } 170 | } 171 | StoreName::Replace => { self.flags = new_flags; } 172 | StoreName::Add => { 173 | for flag in new_flags { 174 | self.flags.insert(flag); 175 | } 176 | } 177 | } 178 | 179 | self.deleted = self.flags.contains(&Flag::Deleted); 180 | self.print_flags() 181 | } 182 | 183 | /// Goes through the list of attributes, constructing a FETCH response for 184 | /// this message containing the values of the requested attributes 185 | pub fn fetch(&self, attributes: &[Attribute]) -> String { 186 | let mut res = String::new(); 187 | let mut first = true; 188 | for attr in attributes.iter() { 189 | // We need to space separate the attribute values 190 | if first { 191 | first = false; 192 | } else { 193 | res.push(' '); 194 | } 195 | 196 | // Provide the attribute name followed by the attribute value 197 | match *attr { 198 | Envelope => { 199 | res.push_str("ENVELOPE "); 200 | res.push_str(&self.mime_message.get_envelope()[..]); 201 | }, 202 | Flags => { 203 | res.push_str("FLAGS "); 204 | res.push_str(&self.print_flags()[..]); 205 | }, 206 | InternalDate => { 207 | res.push_str("INTERNALDATE \""); 208 | res.push_str(&self.date_received()[..]); 209 | res.push('"'); 210 | } 211 | RFC822(ref attr) => { 212 | res.push_str("RFC822"); 213 | match *attr { 214 | AllRFC822 | TextRFC822 => {}, 215 | HeaderRFC822 => { 216 | res.push_str(".HEADER {"); 217 | res.push_str(&self.mime_message.get_header_boundary()[..]); 218 | res.push_str("}\r\n"); 219 | res.push_str(self.mime_message.get_header()); 220 | }, 221 | SizeRFC822 => { 222 | res.push_str(".SIZE "); 223 | res.push_str(&self.mime_message.get_size()[..]) }, 224 | }; 225 | }, 226 | Body | BodyStructure => {}, 227 | BodySection(ref section, ref octets) | 228 | BodyPeek(ref section, ref octets) => { 229 | res.push_str(&self.mime_message.get_body(section, octets)[..]) }, 230 | /* 231 | BodyStructure => { 232 | let content_type: Vec<&str> = (&self.headers["CONTENT-TYPE".to_string()][..]).splitn(2, ';').take(1).collect(); 233 | let content_type: Vec<&str> = content_type[0].splitn(2, '/').collect(); 234 | 235 | // Retrieve the subtype of the content type. 236 | let mut subtype = String::new(); 237 | if content_type.len() > 1 { subtype = content_type[1].to_ascii_uppercase() } 238 | 239 | let content_type = content_type[0].to_ascii_uppercase(); 240 | println!("Content-type: {}/{}", content_type, subtype); 241 | match &content_type[..] { 242 | "MESSAGE" => { 243 | match &subtype[..] { 244 | "RFC822" => { 245 | // Immediately after the basic fields, add the envelope 246 | // structure, body structure, and size in text lines of 247 | // the encapsulated message. 248 | }, 249 | _ => { }, 250 | } 251 | }, 252 | "TEXT" => { 253 | // Immediately after the basic fields, add the size of the body 254 | // in text lines. This is the size in the content transfer 255 | // encoding and not the size after any decoding. 256 | }, 257 | "MULTIPART" => { 258 | 259 | }, 260 | _ => { }, 261 | } 262 | }, 263 | */ 264 | UID => { 265 | res.push_str("UID "); 266 | res.push_str(&self.uid.to_string()[..]) 267 | } 268 | } 269 | } 270 | res 271 | } 272 | 273 | // Creates a string of the current set of flags based on what is in 274 | // self.flags. 275 | fn print_flags(&self) -> String { 276 | let mut res = "(".to_string(); 277 | let mut first = true; 278 | for flag in &self.flags { 279 | // The flags should be space separated. 280 | if first { 281 | first = false; 282 | } else { 283 | res.push(' '); 284 | } 285 | let flag_str = match *flag { 286 | Flag::Answered => { "\\Answered" }, 287 | Flag::Draft => { "\\Draft" }, 288 | Flag::Flagged => { "\\Flagged" }, 289 | Flag::Seen => { "\\Seen" } 290 | Flag::Deleted => { "\\Deleted" } 291 | }; 292 | res.push_str(flag_str); 293 | } 294 | res.push(')'); 295 | res 296 | } 297 | 298 | /// Creates a new filename using the convention that we use while parsing 299 | /// the message's filename. UID followed by a colon, then 2, then the 300 | /// single character per flag representation of the current set of flags. 301 | pub fn get_new_filename(&self) -> String { 302 | let mut res = self.uid.to_string(); 303 | 304 | // it is just the UID if no flags are set. 305 | if self.flags.is_empty() { 306 | return res; 307 | } 308 | 309 | // Add the prelud which separates the flags 310 | res.push_str(":2,"); 311 | 312 | // As per the Maildir standard, the flags are to be written in 313 | // alphabetical order 314 | if self.flags.contains(&Flag::Draft) { 315 | res.push('D'); 316 | } 317 | if self.flags.contains(&Flag::Flagged) { 318 | res.push('F'); 319 | } 320 | if self.flags.contains(&Flag::Answered) { 321 | res.push('R'); 322 | } 323 | if self.flags.contains(&Flag::Seen) { 324 | res.push('S'); 325 | } 326 | res 327 | } 328 | 329 | fn date_received(&self) -> String { 330 | // Retrieve the date received from the UID. 331 | let date_received = Timespec { sec: self.uid as i64, nsec: 0i32 }; 332 | let date_received_tm = time::at_utc(date_received); 333 | 334 | let month = match date_received_tm.tm_mon { 335 | 0 => "Jan", 336 | 1 => "Feb", 337 | 2 => "Mar", 338 | 3 => "Apr", 339 | 4 => "May", 340 | 5 => "Jun", 341 | 6 => "Jul", 342 | 7 => "Aug", 343 | 8 => "Sep", 344 | 9 => "Oct", 345 | 10 => "Nov", 346 | 11 => "Dec", 347 | // NOTE: this should never happen. 348 | _ => panic!("Unable to determine month!") 349 | }; 350 | 351 | format!( 352 | "{:0>2}-{}-{:0>2} {:0>2}:{:0>2}:{:0>2} -0000", 353 | date_received_tm.tm_mday, 354 | month, 355 | date_received_tm.tm_year + 1900i32, 356 | date_received_tm.tm_hour, 357 | date_received_tm.tm_min, 358 | date_received_tm.tm_sec) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /core/src/parser/error.rs: -------------------------------------------------------------------------------- 1 | use nom; 2 | use std::error::Error as StdError; 3 | use std::fmt; 4 | use std::result::Result as StdResult; 5 | 6 | /// A convenient alias type for results for `parser`. 7 | pub type Result = StdResult; 8 | 9 | /// Represents errors which occur while parsing. 10 | #[derive(Debug, PartialEq)] 11 | pub enum Error { 12 | /// An internal `nom` error. 13 | Nom(nom::Err), 14 | /// Incomplete input was fed to the parser. 15 | Incomplete, 16 | } 17 | 18 | impl fmt::Display for Error { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | use self::Error::*; 21 | 22 | match *self { 23 | Incomplete => write!(f, "{}", StdError::description(self)), 24 | Nom(ref e) => e.fmt(f), 25 | } 26 | } 27 | } 28 | 29 | impl StdError for Error { 30 | fn description(&self) -> &str { 31 | use self::Error::*; 32 | 33 | match *self { 34 | Incomplete => "Incomplete input was fed to the parser.", 35 | Nom(ref e) => e.description(), 36 | } 37 | } 38 | 39 | fn cause(&self) -> Option<&StdError> { 40 | use self::Error::*; 41 | 42 | match *self { 43 | Incomplete => None, 44 | Nom(ref e) => e.cause(), 45 | } 46 | } 47 | } 48 | 49 | impl From for Error { 50 | fn from(error: nom::Err) -> Error { 51 | Error::Nom(error) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/parser/grammar/fetch.rs: -------------------------------------------------------------------------------- 1 | use command::Attribute::{ 2 | self, 3 | Body, 4 | BodyPeek, 5 | BodySection, 6 | BodyStructure, 7 | Envelope, 8 | Flags, 9 | InternalDate, 10 | RFC822, 11 | UID 12 | }; 13 | use command::FetchCommand; 14 | use command::RFC822Attribute::{AllRFC822, HeaderRFC822, SizeRFC822, TextRFC822}; 15 | use mime::BodySectionType::{self, AllSection, MsgtextSection, PartSection}; 16 | use mime::Msgtext::{ 17 | self, 18 | HeaderFieldsMsgtext, 19 | HeaderFieldsNotMsgtext, 20 | HeaderMsgtext, 21 | MimeMsgtext, 22 | TextMsgtext, 23 | }; 24 | use parser::grammar::{astring, number, nz_number, whitespace}; 25 | use parser::grammar::sequence::sequence_set; 26 | use std::ascii::AsciiExt; 27 | use std::str; 28 | 29 | named!(pub fetch, 30 | do_parse!( 31 | tag_no_case!("FETCH") >> 32 | whitespace >> 33 | set: sequence_set >> 34 | whitespace >> 35 | attrs: alt!( 36 | delimited!( 37 | tag!("("), 38 | do_parse!( 39 | a: fetch_att >> 40 | b: many0!(preceded!(whitespace, fetch_att)) >> 41 | 42 | ({ 43 | let mut attrs = vec![a]; 44 | for attr in b { 45 | attrs.push(attr); 46 | } 47 | attrs 48 | }) 49 | ), 50 | tag!(")") 51 | ) | 52 | map!(fetch_att, |attr| { vec![attr] }) | 53 | map!(tag_no_case!("ALL"), |_| { vec![Flags, InternalDate, RFC822(SizeRFC822), Envelope] }) | 54 | map!(tag_no_case!("FULL"), |_| { vec![Flags, InternalDate, RFC822(SizeRFC822), Envelope, Body] }) | 55 | map!(tag_no_case!("FAST"), |_| { vec![Flags, InternalDate, RFC822(SizeRFC822)] }) 56 | ) >> 57 | 58 | ({ FetchCommand::new(set, attrs) }) 59 | ) 60 | ); 61 | 62 | named!(fetch_att, 63 | alt!( 64 | complete!(tag_no_case!("ENVELOPE")) => { |_| { Envelope } } | 65 | complete!(tag_no_case!("FLAGS")) => { |_| { Flags } } | 66 | complete!(tag_no_case!("INTERNALDATE")) => { |_| { InternalDate } } | 67 | do_parse!( 68 | tag_no_case!("RFC822") >> 69 | sub_attr: opt!(alt!( 70 | tag!(".HEADER") => { |_| { HeaderRFC822 } } | 71 | tag!(".SIZE") => { |_| { SizeRFC822 } } | 72 | tag!(".TEST") => { |_| { TextRFC822 } } 73 | )) >> 74 | 75 | ({ RFC822(sub_attr.unwrap_or(AllRFC822)) }) 76 | ) | 77 | complete!(tag_no_case!("UID")) => { |_| { UID } } | 78 | preceded!( 79 | tag_no_case!("BODY"), 80 | alt!( 81 | do_parse!( 82 | tag_no_case!(".PEEK") >> 83 | section: section >> 84 | octets: opt!(octet_range) >> 85 | 86 | ({ BodyPeek(section, octets) }) 87 | ) | 88 | do_parse!( 89 | section: section >> 90 | octets: opt!(octet_range) >> 91 | 92 | ({ BodySection(section, octets) }) 93 | ) | 94 | do_parse!( 95 | sub_attr: opt!(tag!("STRUCTURE")) >> 96 | 97 | ({ 98 | if sub_attr.is_some() { 99 | BodyStructure 100 | } else { 101 | Body 102 | } 103 | }) 104 | ) 105 | ) 106 | ) 107 | ) 108 | ); 109 | 110 | named!(octet_range<(usize, usize)>, 111 | delimited!( 112 | tag!("<"), 113 | do_parse!( 114 | first_octet: number >> 115 | tag!(".") >> 116 | last_octet: nz_number >> 117 | 118 | ((first_octet, last_octet)) 119 | ), 120 | tag!(">") 121 | ) 122 | ); 123 | 124 | /* Section parsing */ 125 | 126 | named!(section, 127 | delimited!( 128 | tag!("["), 129 | map!( 130 | opt!(section_spec), 131 | |v: Option| { v.unwrap_or(AllSection) } 132 | ), 133 | tag!("]") 134 | ) 135 | ); 136 | 137 | named!(section_spec, 138 | alt!( 139 | section_msgtext => { |v| { MsgtextSection(v) } } | 140 | do_parse!( 141 | a: section_part >> 142 | b: opt!(preceded!(tag!("."), section_text)) >> 143 | 144 | ({ PartSection(a, b) }) 145 | ) 146 | ) 147 | ); 148 | 149 | // Top-level or MESSAGE/RFC822 part 150 | named!(section_msgtext, 151 | alt!( 152 | complete!(do_parse!( 153 | tag_no_case!("HEADER.FIELDS") >> 154 | not: opt!(tag_no_case!(".NOT")) >> 155 | whitespace >> 156 | headers: header_list >> 157 | 158 | ({ 159 | if not.is_some() { 160 | HeaderFieldsNotMsgtext(headers) 161 | } else { 162 | HeaderFieldsMsgtext(headers) 163 | } 164 | }) 165 | )) | 166 | complete!(tag_no_case!("HEADER")) => { |_| { HeaderMsgtext } } | 167 | tag_no_case!("TEXT") => { |_| { TextMsgtext } } 168 | ) 169 | ); 170 | 171 | named!(header_list>, 172 | delimited!( 173 | tag!("("), 174 | separated_nonempty_list!(whitespace, header_fld_name), 175 | tag!(")") 176 | ) 177 | ); 178 | 179 | named!(header_fld_name, 180 | map!( 181 | map_res!(astring, str::from_utf8), 182 | AsciiExt::to_ascii_uppercase 183 | ) 184 | ); 185 | 186 | // Body part nesting 187 | named!(section_part>, 188 | separated_nonempty_list!(tag!("."), nz_number) 189 | ); 190 | 191 | // Text other than actual body part (headers, etc.) 192 | named!(section_text, 193 | alt!( 194 | section_msgtext | 195 | tag_no_case!("MIME") => { |_| { MimeMsgtext } } 196 | ) 197 | ); 198 | 199 | #[cfg(test)] 200 | mod tests { 201 | use command::Attribute::{ 202 | Body, 203 | BodyPeek, 204 | BodySection, 205 | BodyStructure, 206 | Envelope, 207 | Flags, 208 | InternalDate, 209 | RFC822, 210 | }; 211 | use command::FetchCommand; 212 | use mime::BodySectionType::{ 213 | AllSection, 214 | MsgtextSection, 215 | PartSection 216 | }; 217 | use mime::Msgtext::{ 218 | HeaderMsgtext, 219 | HeaderFieldsMsgtext, 220 | HeaderFieldsNotMsgtext, 221 | MimeMsgtext, 222 | TextMsgtext 223 | }; 224 | use command::RFC822Attribute::{ 225 | AllRFC822, 226 | HeaderRFC822, 227 | SizeRFC822, 228 | }; 229 | use command::sequence_set::SequenceItem::{ 230 | Number, 231 | Range, 232 | Wildcard 233 | }; 234 | use nom::ErrorKind::{Alt, OneOf, Tag}; 235 | use nom::Needed::Size; 236 | use nom::IResult::{Done, Error, Incomplete}; 237 | use super::{ 238 | fetch, 239 | fetch_att, 240 | header_fld_name, 241 | header_list, 242 | octet_range, 243 | section, 244 | section_msgtext, 245 | section_part, 246 | section_spec, 247 | section_text, 248 | }; 249 | 250 | #[test] 251 | fn test_fetch() { 252 | assert_eq!(fetch(b""), Incomplete(Size(5))); 253 | assert_eq!(fetch(b"FETCH *:3,6 (FLAGS RFC822)"), Done(&b""[..], 254 | FetchCommand::new(vec![Range(Box::new(Wildcard), Box::new(Number(3))), Number(6)], vec![Flags, RFC822(AllRFC822)]) 255 | )); 256 | assert_eq!(fetch(b"FETCH * FLAGS"), Done(&b""[..], 257 | FetchCommand::new(vec![Wildcard], vec![Flags]) 258 | )); 259 | assert_eq!(fetch(b"FETCH * ALL"), Done(&b""[..], 260 | FetchCommand::new(vec![Wildcard], vec![Flags, InternalDate, RFC822(SizeRFC822), Envelope]) 261 | )); 262 | assert_eq!(fetch(b"FETCH * FULL"), Done(&b""[..], 263 | FetchCommand::new(vec![Wildcard], vec![Flags, InternalDate, RFC822(SizeRFC822), Envelope, Body]) 264 | )); 265 | assert_eq!(fetch(b"FETCH * FAST"), Done(&b""[..], 266 | FetchCommand::new(vec![Wildcard], vec![Flags, InternalDate, RFC822(SizeRFC822)]) 267 | )); 268 | assert_eq!( 269 | fetch(b"FETCH 4,5:3,* (FLAGS RFC822 BODY.PEEK[43.65.HEADER.FIELDS.NOT (abc \"def\" {2}\r\nde)]<4.2>)"), 270 | Done(&b""[..], 271 | FetchCommand::new( 272 | vec![Number(4), Range(Box::new(Number(5)), Box::new(Number(3))), Wildcard], 273 | vec![ 274 | Flags, 275 | RFC822(AllRFC822), 276 | BodyPeek( 277 | PartSection( 278 | vec![43, 65], 279 | Some(HeaderFieldsNotMsgtext(vec![ 280 | "ABC".to_owned(), 281 | "DEF".to_owned(), 282 | "DE".to_owned() 283 | ])) 284 | ), 285 | Some((4, 2)) 286 | ) 287 | ] 288 | ) 289 | ) 290 | ); 291 | } 292 | 293 | #[test] 294 | fn test_fetch_case_insensitivity() { 295 | assert_eq!( 296 | fetch(b"FETCH * (FLAGS BODY[4.2.2.2.HEADER.FIELDS.NOT (DATE FROM)]<400.10000>)"), 297 | fetch(b"fetch * (flags body[4.2.2.2.header.fields.not (date from)]<400.10000>)") 298 | ); 299 | } 300 | 301 | #[test] 302 | fn test_fetch_att() { 303 | assert_eq!(fetch_att(b""), Incomplete(Size(6))); 304 | assert_eq!(fetch_att(b"envelope"), Done(&b""[..], Envelope)); 305 | assert_eq!(fetch_att(b"FLAGS"), Done(&b""[..], Flags)); 306 | assert_eq!(fetch_att(b"RFC822 "), Done(&b" "[..], RFC822(AllRFC822))); 307 | assert_eq!(fetch_att(b"RFC822.HEADER"), Done(&b""[..], RFC822(HeaderRFC822))); 308 | assert_eq!(fetch_att(b"BODY "), Done(&b" "[..], 309 | Body 310 | )); 311 | assert_eq!(fetch_att(b"BODYSTRUCTURE"), Done(&b""[..], 312 | BodyStructure 313 | )); 314 | assert_eq!(fetch_att(b"BODY.PEEK[] "), Done(&b" "[..], 315 | BodyPeek(AllSection, None) 316 | )); 317 | assert_eq!(fetch_att(b"BODY.PEEK[]<1.2>"), Done(&b""[..], 318 | BodyPeek(AllSection, Some((1, 2))) 319 | )); 320 | assert_eq!(fetch_att(b"BODY[TEXT]<1.2>"), Done(&b""[..], 321 | BodySection(MsgtextSection(TextMsgtext), Some((1, 2))) 322 | )); 323 | } 324 | 325 | #[test] 326 | fn test_octet_range() { 327 | assert_eq!(octet_range(b""), Incomplete(Size(1))); 328 | assert_eq!(octet_range(b"<0.0>"), Error(OneOf)); 329 | assert_eq!(octet_range(b"<100.200>"), Done(&b""[..], (100, 200))); 330 | } 331 | 332 | #[test] 333 | fn test_section() { 334 | assert_eq!(section(b""), Incomplete(Size(1))); 335 | assert_eq!(section(b"[]"), Done(&b""[..], AllSection)); 336 | assert_eq!(section(b"[1.2.3.HEADER.FIELDS (abc def)]"), 337 | Done( 338 | &b""[..], 339 | PartSection( 340 | vec![1, 2, 3], 341 | Some(HeaderFieldsMsgtext(vec!["ABC".to_string(), "DEF".to_string()])) 342 | ) 343 | ) 344 | ); 345 | } 346 | 347 | #[test] 348 | fn test_section_spec() { 349 | assert_eq!(section_spec(b""), Incomplete(Size(4))); 350 | assert_eq!(section_spec(b"invalid"), Error(Alt)); 351 | assert_eq!(section_spec(b"HEADER"), Done(&b""[..], MsgtextSection(HeaderMsgtext))); 352 | assert_eq!(section_spec(b"1.2.3.MIME"), Done(&b""[..], PartSection(vec![1, 2, 3], Some(MimeMsgtext)))); 353 | assert_eq!(section_spec(b"1.2.3.HEADER.FIELDS (abc def)"), 354 | Done( 355 | &b""[..], 356 | PartSection( 357 | vec![1, 2, 3], 358 | Some(HeaderFieldsMsgtext(vec!["ABC".to_string(), "DEF".to_string()])) 359 | ) 360 | ) 361 | ); 362 | } 363 | 364 | #[test] 365 | fn test_section_msgtext() { 366 | assert_eq!(section_msgtext(b""), Incomplete(Size(4))); 367 | assert_eq!(section_msgtext(b"invalid"), Error(Alt)); 368 | assert_eq!(section_msgtext(b"header"), Done(&b""[..], HeaderMsgtext)); 369 | assert_eq!(section_msgtext(b"HEADER"), Done(&b""[..], HeaderMsgtext)); 370 | assert_eq!(section_msgtext(b"text"), Done(&b""[..], TextMsgtext)); 371 | assert_eq!(section_msgtext(b"HEADER.FIELDS (abc def)"), 372 | Done( 373 | &b""[..], 374 | HeaderFieldsMsgtext(vec!["ABC".to_string(), "DEF".to_string()]) 375 | ) 376 | ); 377 | assert_eq!(section_msgtext(b"HEADER.FIELDS.NOT (abc def)"), 378 | Done( 379 | &b""[..], 380 | HeaderFieldsNotMsgtext(vec!["ABC".to_string(), "DEF".to_string()]) 381 | ) 382 | ); 383 | } 384 | 385 | #[test] 386 | fn test_header_list() { 387 | assert_eq!(header_list(b""), Incomplete(Size(1))); 388 | assert_eq!(header_list(b"(abc\ndef)"), Error(Tag)); 389 | assert_eq!(header_list(b"(abc)\ndef"), Done(&b"\ndef"[..], vec!["ABC".to_string()])); 390 | assert_eq!(header_list(b"(abc def ghi jkl)"), 391 | Done(&b""[..], vec!["ABC".to_string(), "DEF".to_string(), "GHI".to_string(), "JKL".to_string()]) 392 | ); 393 | assert_eq!(header_list(b"({3}\r\ndef)"), 394 | Done(&b""[..], vec!["DEF".to_string()]) 395 | ); 396 | } 397 | 398 | #[test] 399 | fn test_header_fld_name() { 400 | assert_eq!(header_fld_name(b""), Incomplete(Size(1))); 401 | assert_eq!(header_fld_name(b"abc123\ndef456"), Done(&b"\ndef456"[..], "ABC123".to_string())); 402 | assert_eq!(header_fld_name(b"{3}\r\ndef"), Done(&b""[..], "DEF".to_string())); 403 | } 404 | 405 | #[test] 406 | fn test_section_part() { 407 | assert_eq!(section_part(b""), Incomplete(Size(1))); 408 | assert_eq!(section_part(b"0"), Error(OneOf)); 409 | assert_eq!(section_part(b"1"), Incomplete(Size(2))); 410 | assert_eq!(section_part(b"1 "), Done(&b" "[..], vec![1])); 411 | assert_eq!(section_part(b"1.2.3 "), Done(&b" "[..], vec![1, 2, 3])); 412 | } 413 | 414 | #[test] 415 | fn test_section_text() { 416 | assert_eq!(section_text(b""), Incomplete(Size(4))); 417 | assert_eq!(section_text(b"MIME"), Done(&b""[..], MimeMsgtext)); 418 | assert_eq!(section_text(b"invalid"), Error(Alt)); 419 | assert_eq!(section_text(b"HEADER.FIELDS (abc def)"), 420 | Done( 421 | &b""[..], 422 | HeaderFieldsMsgtext(vec!["ABC".to_string(), "DEF".to_string()]) 423 | ) 424 | ); 425 | } 426 | } 427 | 428 | #[cfg(all(feature = "unstable", test))] 429 | mod bench { 430 | extern crate test; 431 | 432 | use command::Attribute::{BodyPeek, Flags, RFC822}; 433 | use command::FetchCommand; 434 | use command::RFC822Attribute::AllRFC822; 435 | use command::sequence_set::SequenceItem::{Number, Range, Wildcard}; 436 | use mime::BodySectionType::PartSection; 437 | use mime::Msgtext::HeaderFieldsNotMsgtext; 438 | use nom::IResult::Done; 439 | use self::test::Bencher; 440 | use super::fetch; 441 | 442 | #[bench] 443 | fn bench_fetch(b: &mut Bencher) { 444 | const FETCH_STR: &'static str = "FETCH 4,5:3,* (FLAGS RFC822 BODY.PEEK[43.65.HEADER.FIELDS.NOT (a \"abc\")]<4.2>)"; 445 | 446 | b.iter(|| { 447 | assert_eq!(fetch(FETCH_STR.as_bytes()), Done(&b""[..], 448 | FetchCommand::new( 449 | vec![Number(4), Range(Box::new(Number(5)), Box::new(Number(3))), Wildcard], 450 | vec![ 451 | Flags, 452 | RFC822(AllRFC822), 453 | BodyPeek( 454 | PartSection( 455 | vec![43, 65], 456 | Some(HeaderFieldsNotMsgtext(vec![ 457 | "A".to_owned(), 458 | "ABC".to_owned(), 459 | ])) 460 | ), 461 | Some((4, 2)) 462 | ) 463 | ] 464 | ) 465 | )); 466 | }); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /core/src/parser/grammar/mod.rs: -------------------------------------------------------------------------------- 1 | use nom::{crlf, Slice}; 2 | use std::str; 3 | 4 | pub use self::fetch::fetch; 5 | 6 | mod fetch; 7 | mod sequence; 8 | 9 | const DIGITS: &'static str = "0123456789"; 10 | const NZ_DIGITS: &'static str = "123456789"; 11 | 12 | fn is_astring_char(chr: u8) -> bool { 13 | is_atom_char(chr) || is_resp_specials(chr) 14 | } 15 | 16 | // any CHAR except atom-specials 17 | fn is_atom_char(chr: u8) -> bool { 18 | !is_atom_specials(chr) 19 | } 20 | 21 | fn is_atom_specials(chr: u8) -> bool { 22 | chr == b'(' || chr == b')' || chr == b'{' || is_sp(chr) || is_ctl(chr) || 23 | is_list_wildcards(chr) || is_quoted_specials(chr) || 24 | is_resp_specials(chr) 25 | } 26 | 27 | fn is_sp(chr: u8) -> bool { 28 | chr == b' ' 29 | } 30 | 31 | fn is_ctl(chr: u8) -> bool { 32 | chr <= b'\x1F' || chr == b'\x7F' 33 | } 34 | 35 | fn is_list_wildcards(chr: u8) -> bool { 36 | chr == b'%' || chr == b'*' 37 | } 38 | 39 | fn is_quoted_specials(chr: u8) -> bool { 40 | is_dquote(chr) || chr == b'\\' 41 | } 42 | 43 | fn is_dquote(chr: u8) -> bool { 44 | chr == b'"' 45 | } 46 | 47 | fn is_resp_specials(chr: u8) -> bool { 48 | chr == b']' 49 | } 50 | 51 | // an ASCII digit (%x30-%x39) 52 | fn is_digit(chr: u8) -> bool { 53 | chr >= b'0' && chr <= b'9' 54 | } 55 | 56 | // any TEXT_CHAR except quoted_specials 57 | fn is_quoted_char(chr: u8) -> bool { 58 | !is_quoted_specials(chr) && is_text_char(chr) 59 | } 60 | 61 | // any CHAR except CR and LF 62 | fn is_text_char(chr: u8) -> bool { 63 | !is_eol_char(chr) 64 | } 65 | 66 | // a CR or LF CHAR 67 | fn is_eol_char(chr: u8) -> bool { 68 | chr == b'\r' || chr == b'\n' 69 | } 70 | 71 | /* String parsing */ 72 | 73 | named!(astring<&[u8], &[u8]>, alt!(take_while1!(is_astring_char) | string)); 74 | 75 | named!(string<&[u8], &[u8]>, alt!(quoted | literal)); 76 | 77 | named!(quoted<&[u8], &[u8]>, 78 | delimited!( 79 | tag!("\""), 80 | recognize!( 81 | many0!( 82 | alt!( 83 | take_while1!(is_quoted_char) | 84 | preceded!(tag!("\\"), alt!(tag!("\"") | tag!("\\"))) 85 | ) 86 | ) 87 | ), 88 | tag!("\"") 89 | ) 90 | ); 91 | 92 | named!(literal<&[u8], &[u8]>, 93 | do_parse!( 94 | // The "number" is used to indicate the number of octets. 95 | number: terminated!(delimited!(tag!("{"), number, tag!("}")), crlf) >> 96 | v: recognize!( 97 | count!( 98 | do_parse!( 99 | // any OCTET except NUL ('%x00') 100 | not!(char!('\x00')) >> 101 | chr: take!(1) >> 102 | 103 | (chr) 104 | ), 105 | number 106 | ) 107 | ) >> 108 | 109 | (v) 110 | ) 111 | ); 112 | 113 | /* RFC 3501 Boilerplate */ 114 | 115 | /// Recognizes an non-zero unsigned 32-bit integer. 116 | // (0 < n < 4,294,967,296) 117 | named!(number, flat_map!(take_while1!(is_digit), parse_to!(usize))); 118 | 119 | /// Recognizes a non-zero unsigned 32-bit integer. 120 | // (0 < n < 4,294,967,296) 121 | named!(nz_number, 122 | flat_map!( 123 | recognize!( 124 | tuple!( 125 | digit_nz, 126 | many0!(one_of!(DIGITS)) 127 | ) 128 | ), 129 | parse_to!(usize) 130 | ) 131 | ); 132 | 133 | /// Recognizes exactly one non-zero numerical character: 1-9. 134 | // digit-nz = %x31-39 135 | // ; 1-9 136 | named!(digit_nz, one_of!(NZ_DIGITS)); 137 | 138 | /// Recognizes exactly one ASCII whitespace. 139 | named!(whitespace, char!(' ')); 140 | 141 | #[cfg(test)] 142 | mod tests { 143 | use nom::ErrorKind::{Alt, Char, Count, OneOf, TakeWhile1, MapOpt, Tag}; 144 | use nom::Needed::Size; 145 | use nom::IResult::{Done, Error, Incomplete}; 146 | use super::{ 147 | astring, 148 | digit_nz, 149 | literal, 150 | number, 151 | nz_number, 152 | quoted, 153 | string, 154 | whitespace 155 | }; 156 | 157 | #[test] 158 | fn test_astring() { 159 | assert_eq!(astring(b""), Incomplete(Size(1))); 160 | assert_eq!(astring(b"("), Error(Alt)); 161 | assert_eq!(astring(b"]"), Done(&b""[..], &b"]"[..])); 162 | assert_eq!(astring(b"a"), Done(&b""[..], &b"a"[..])); 163 | assert_eq!(astring(b"\"test\"abc"), Done(&b"abc"[..], &b"test"[..])); 164 | assert_eq!(astring(b"\"test\""), Done(&b""[..], &b"test"[..])); 165 | assert_eq!(astring(b"{3}\r\nabc\x00"), Done(&b"\x00"[..], &b"abc"[..])); 166 | } 167 | 168 | #[test] 169 | fn test_string() { 170 | assert_eq!(string(b"\"test\""), Done(&b""[..], &b"test"[..])); 171 | assert_eq!(string(b"{2}\r\nab\x00"), Done(&b"\x00"[..], &b"ab"[..])); 172 | } 173 | 174 | #[test] 175 | fn test_quoted() { 176 | assert_eq!(quoted(b""), Incomplete(Size(1))); 177 | assert_eq!(quoted(b"\"\""), Done(&b""[..], &b""[..])); 178 | assert_eq!(quoted(b"\"a\""), Done(&b""[..], &b"a"[..])); 179 | assert_eq!(quoted(b"\"\\\""), Incomplete(Size(4))); 180 | assert_eq!(quoted(b"\"\\\"\""), Done(&b""[..], &b"\\\""[..])); 181 | assert_eq!(quoted(b"\"\\\\\""), Done(&b""[..], &b"\\\\"[..])); 182 | assert_eq!(quoted(b"\"\r\""), Error(Tag)); 183 | assert_eq!(quoted(b"\"\t\""), Done(&b""[..], &b"\t"[..])); 184 | } 185 | 186 | #[test] 187 | fn test_literal() { 188 | assert_eq!(literal(b""), Incomplete(Size(1))); 189 | assert_eq!(literal(b"{1}\r\nabc"), Done(&b"bc"[..], &b"a"[..])); 190 | assert_eq!(literal(b"{0}\r\n"), Done(&b""[..], &b""[..])); 191 | assert_eq!(literal(b"{1}\r\na"), Done(&b""[..], &b"a"[..])); 192 | assert_eq!(literal(b"{2}\r\na"), Incomplete(Size(7))); 193 | assert_eq!(literal(b"{2}\r\na\x00a"), Error(Count)); 194 | } 195 | 196 | #[test] 197 | fn test_number() { 198 | assert_eq!(number(b""), Incomplete(Size(1))); 199 | assert_eq!(number(b"a"), Error(TakeWhile1)); 200 | assert_eq!(number(b"0"), Done(&b""[..], 0)); 201 | assert_eq!(number(b"1"), Done(&b""[..], 1)); 202 | assert_eq!(number(b"10"), Done(&b""[..], 10)); 203 | assert_eq!(number(b"10a"), Done(&b"a"[..], 10)); 204 | assert_eq!(number(b"4294967296"), Done(&b""[..], 4294967296)); 205 | assert_eq!(number(b"100000000000000000000"), Error(MapOpt)); 206 | } 207 | 208 | #[test] 209 | fn test_nz_number() { 210 | assert_eq!(nz_number(b""), Incomplete(Size(1))); 211 | assert_eq!(nz_number(b"a"), Error(OneOf)); 212 | assert_eq!(nz_number(b"0"), Error(OneOf)); 213 | assert_eq!(nz_number(b"1"), Done(&b""[..], 1)); 214 | assert_eq!(nz_number(b"10"), Done(&b""[..], 10)); 215 | assert_eq!(nz_number(b"10a"), Done(&b"a"[..], 10)); 216 | assert_eq!(nz_number(b"4294967296"), Done(&b""[..], 4294967296)); 217 | assert_eq!(nz_number(b"100000000000000000000"), Error(MapOpt)); 218 | } 219 | 220 | #[test] 221 | fn test_digit_nz() { 222 | assert_eq!(digit_nz(b""), Incomplete(Size(1))); 223 | assert_eq!(digit_nz(b"a"), Error(OneOf)); 224 | assert_eq!(digit_nz(b"1"), Done(&b""[..], '1')); 225 | assert_eq!(digit_nz(b"62"), Done(&b"2"[..], '6')); 226 | } 227 | 228 | #[test] 229 | fn test_whitespace() { 230 | assert_eq!(whitespace(b""), Incomplete(Size(1))); 231 | assert_eq!(whitespace(b"a"), Error(Char)); 232 | assert_eq!(whitespace(b" "), Done(&b""[..], ' ')); 233 | assert_eq!(whitespace(b"\t"), Error(Char)); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /core/src/parser/grammar/sequence.rs: -------------------------------------------------------------------------------- 1 | use command::sequence_set::SequenceItem; 2 | use parser::grammar::nz_number; 3 | 4 | /* Sequence item and set rules */ 5 | 6 | named!(pub sequence_set>, 7 | do_parse!( 8 | a: alt!( 9 | complete!(seq_range) | 10 | seq_number 11 | ) >> 12 | b: many0!(preceded!(tag!(","), sequence_set)) >> 13 | 14 | ({ 15 | let mut seq: Vec = b.into_iter() 16 | .flat_map(|set| set.into_iter()) 17 | .collect(); 18 | seq.insert(0, a); 19 | 20 | seq 21 | }) 22 | ) 23 | ); 24 | 25 | named!(seq_range, 26 | do_parse!( 27 | a: seq_number >> 28 | tag!(":") >> 29 | b: seq_number >> 30 | 31 | (SequenceItem::Range(Box::new(a), Box::new(b))) 32 | ) 33 | ); 34 | 35 | named!(seq_number, 36 | alt!( 37 | nz_number => { |num: usize| SequenceItem::Number(num) } | 38 | tag!("*") => { |_| SequenceItem::Wildcard } 39 | ) 40 | ); 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use command::sequence_set::SequenceItem::{ 45 | Number, 46 | Range, 47 | Wildcard 48 | }; 49 | use nom::ErrorKind::Alt; 50 | use nom::Needed::Size; 51 | use nom::IResult::{Done, Error, Incomplete}; 52 | use super::{ 53 | sequence_set, 54 | seq_number, 55 | seq_range, 56 | }; 57 | 58 | #[test] 59 | fn test_sequence_set() { 60 | assert_eq!(sequence_set(b""), Incomplete(Size(1))); 61 | assert_eq!(sequence_set(b"a"), Error(Alt)); 62 | assert_eq!(sequence_set(b"0"), Error(Alt)); 63 | assert_eq!(sequence_set(b"a:*"), Error(Alt)); 64 | assert_eq!(sequence_set(b":*"), Error(Alt)); 65 | assert_eq!(sequence_set(b"*"), Done(&b""[..], vec![Wildcard])); 66 | assert_eq!(sequence_set(b"1"), Done(&b""[..], vec![Number(1)])); 67 | assert_eq!(sequence_set(b"1:"), Done(&b":"[..], vec![Number(1)])); 68 | assert_eq!(sequence_set(b"4,5,6,"), Incomplete(Size(7))); 69 | assert_eq!(sequence_set(b"1:0"), Done(&b":0"[..], vec![Number(1)])); 70 | assert_eq!(sequence_set(b"0:1"), Error(Alt)); 71 | assert_eq!(sequence_set(b"1:1"), Done(&b""[..], vec![ 72 | Range(Box::new(Number(1)), Box::new(Number(1))) 73 | ])); 74 | assert_eq!(sequence_set(b"2:4a"), Done(&b"a"[..], vec![ 75 | Range(Box::new(Number(2)), Box::new(Number(4))) 76 | ])); 77 | assert_eq!(sequence_set(b"*:3, 4:4"), Done(&b", 4:4"[..], vec![ 78 | Range(Box::new(Wildcard), Box::new(Number(3))) 79 | ])); 80 | assert_eq!(sequence_set(b"*:3,4:4"), Done(&b""[..], vec![ 81 | Range(Box::new(Wildcard), Box::new(Number(3))), 82 | Range(Box::new(Number(4)), Box::new(Number(4))) 83 | ])); 84 | } 85 | 86 | #[test] 87 | fn test_seq_range() { 88 | assert_eq!(seq_range(b""), Incomplete(Size(1))); 89 | assert_eq!(seq_range(b"a"), Error(Alt)); 90 | assert_eq!(seq_range(b"0"), Error(Alt)); 91 | assert_eq!(seq_range(b"1:1"), Done(&b""[..], Range(Box::new(Number(1)), Box::new(Number(1))))); 92 | assert_eq!(seq_range(b"2:4a"), Done(&b"a"[..], Range(Box::new(Number(2)), Box::new(Number(4))))); 93 | assert_eq!(seq_range(b"*:3"), Done(&b""[..], Range(Box::new(Wildcard), Box::new(Number(3))))); 94 | } 95 | 96 | #[test] 97 | fn test_seq_number() { 98 | assert_eq!(seq_number(b""), Incomplete(Size(1))); 99 | assert_eq!(seq_number(b"a"), Error(Alt)); 100 | assert_eq!(seq_number(b"0"), Error(Alt)); 101 | assert_eq!(seq_number(b"100"), Done(&b""[..], Number(100))); 102 | assert_eq!(seq_number(b"*a"), Done(&b"a"[..], Wildcard)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /core/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | use command::FetchCommand; 2 | 3 | mod error; 4 | mod grammar; 5 | 6 | pub use self::error::Error as ParserError; 7 | pub use self::error::Result as ParserResult; 8 | 9 | pub fn fetch(input: &[u8]) -> ParserResult { 10 | use nom::IResult::{Done, Error, Incomplete}; 11 | 12 | match self::grammar::fetch(input) { 13 | Done(_, v) => Ok(v), 14 | Incomplete(_) => Err(ParserError::Incomplete), 15 | Error(err) => Err(err).map_err(ParserError::from), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/server/config.rs: -------------------------------------------------------------------------------- 1 | use error::ImapResult; 2 | use openssl::error::ErrorStack; 3 | use openssl::pkcs12::Pkcs12; 4 | use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslMethod}; 5 | use std::io::{Read, Error as IoError, Write}; 6 | use std::fs::File; 7 | use std::path::Path; 8 | use std::str; 9 | use toml; 10 | 11 | pub enum PkcsError { 12 | Io(IoError), 13 | Ssl(ErrorStack), 14 | PortsDisabled 15 | } 16 | 17 | impl From for PkcsError { 18 | fn from(e: IoError) -> Self { 19 | PkcsError::Io(e) 20 | } 21 | } 22 | 23 | impl From for PkcsError { 24 | fn from(e: ErrorStack) -> Self { 25 | PkcsError::Ssl(e) 26 | } 27 | } 28 | 29 | /// Representation of configuration data for the server 30 | #[derive(Debug, Serialize, Deserialize)] 31 | pub struct Config { 32 | // Host on which to listen 33 | pub host: String, 34 | // Plaintext port on which to listen for LMTP 35 | pub lmtp_port: Option, 36 | // Plaintext port on which to listen for IMAP 37 | pub imap_port: Option, 38 | // SSL port on which to listen for LMTP 39 | pub lmtp_ssl_port: Option, 40 | // SSL port on which to listen for IMAP 41 | pub imap_ssl_port: Option, 42 | // file in which user data is stored 43 | pub users: String, 44 | // Filename of PKCS #12 archive 45 | pub pkcs_file: String, 46 | // Password for PKCS #12 archive 47 | pub pkcs_pass: String, 48 | } 49 | 50 | impl Config { 51 | pub fn new() -> ImapResult { 52 | let path = Path::new("./config.toml"); 53 | 54 | let config = match File::open(&path) { 55 | Ok(mut file) => { 56 | let mut encoded: String = String::new(); 57 | match file.read_to_string(&mut encoded) { 58 | Ok(_) => match toml::from_str(&encoded) { 59 | Ok(v) => v, 60 | Err(e) => { 61 | // Use default values if parsing failed. 62 | warn!("Failed to parse config.toml.\nUsing default values: {}", e); 63 | Config::default() 64 | }, 65 | }, 66 | Err(e) => { 67 | // Use default values if reading failed. 68 | warn!("Failed to read config.toml.\nUsing default values: {}", e); 69 | Config::default() 70 | }, 71 | } 72 | }, 73 | Err(e) => { 74 | // Create a default config file if it doesn't exist 75 | warn!("Failed to open config.toml; creating from defaults: {}", e); 76 | let config = Config::default(); 77 | let encoded = toml::to_string(&config)?; 78 | let mut file = File::create(&path)?; 79 | file.write_all(encoded.as_bytes())?; 80 | config 81 | }, 82 | }; 83 | 84 | Ok(config) 85 | } 86 | 87 | pub fn get_ssl_acceptor(&self) -> Result { 88 | if self.imap_ssl_port == None && self.lmtp_ssl_port == None { 89 | return Err(PkcsError::PortsDisabled); 90 | } 91 | let mut buf = vec![]; 92 | let mut file = File::open(&self.pkcs_file)?; 93 | file.read_to_end(&mut buf)?; 94 | let p = Pkcs12::from_der(&buf)?; 95 | let identity = p.parse(&self.pkcs_pass)?; 96 | let builder = SslAcceptorBuilder::mozilla_intermediate( 97 | SslMethod::tls(), &identity.pkey, &identity.cert, &identity.chain)?; 98 | Ok(builder.build()) 99 | } 100 | } 101 | 102 | impl Default for Config { 103 | fn default() -> Self { 104 | Config { 105 | host: "127.0.0.1".to_string(), 106 | lmtp_port: Some(3000), 107 | imap_port: Some(10000), 108 | lmtp_ssl_port: None, 109 | imap_ssl_port: Some(10001), 110 | users: "./users.json".to_string(), 111 | pkcs_file: String::new(), 112 | pkcs_pass: String::new(), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /core/src/server/imap.rs: -------------------------------------------------------------------------------- 1 | use std::ascii::AsciiExt; 2 | use std::fs; 3 | use std::io::{BufRead, Write}; 4 | use std::net::TcpStream; 5 | use std::os::unix::fs::PermissionsExt; 6 | use std::path::Path; 7 | use std::path::MAIN_SEPARATOR; 8 | use std::str::Split; 9 | use std::sync::Arc; 10 | use bufstream::BufStream; 11 | use regex::Regex; 12 | 13 | use folder::Folder; 14 | use server::Server; 15 | use server::Stream; 16 | 17 | use command::Attribute::UID; 18 | use command::fetch; 19 | use command::store; 20 | use command::sequence_set; 21 | use command::sequence_set::SequenceItem::{ 22 | Number, 23 | Range, 24 | Wildcard 25 | }; 26 | use error::Error; 27 | use util; 28 | 29 | // Used to grab every file for removal while performing DELETE on a folder. 30 | macro_rules! opendirlisting( 31 | ($inp:expr, $listing:ident, $err:ident, $next:expr) => { 32 | match fs::read_dir($inp) { 33 | Err(_) => return $err, 34 | Ok($listing) => { 35 | $next 36 | } 37 | } 38 | } 39 | ); 40 | 41 | // Standard IMAP greeting 42 | static GREET: &'static [u8] = b"* OK Server ready.\r\n"; 43 | 44 | /// Representation of a session 45 | pub struct ImapSession { 46 | /// Shared wrapper for config and user data 47 | serv: Arc, 48 | /// Whether to logout and close the connection after interpreting the 49 | /// latest client command 50 | logout: bool, 51 | /// If None, not logged in. If Some(String), the String represents the 52 | /// logged in user's maildir 53 | maildir: Option, 54 | /// If None, no folder selected. Otherwise, contains the currently selected 55 | /// folder. 56 | folder: Option 57 | } 58 | 59 | impl ImapSession { 60 | pub fn new(serv: Arc) -> ImapSession { 61 | ImapSession { 62 | serv: serv, 63 | logout: false, 64 | maildir: None, 65 | folder: None 66 | } 67 | } 68 | 69 | /// Handles client commands as they come in on the stream and writes 70 | /// responeses back to the stream. 71 | pub fn handle(&mut self, orig_stream: TcpStream) { 72 | let mut stream = BufStream::new(self.serv.imap_ssl(orig_stream)); 73 | // Provide the client with an IMAP greeting. 74 | return_on_err!(stream.write(GREET)); 75 | return_on_err!(stream.flush()); 76 | 77 | let mut command = String::new(); 78 | loop { 79 | command.truncate(0); 80 | match stream.read_line(&mut command) { 81 | Ok(_) => { 82 | // If the command is empty, exit. 83 | // Exitting will close the stream for us. 84 | if command.is_empty() { 85 | return; 86 | } 87 | 88 | let mut args = command.trim().split(' '); 89 | let inv_str = " BAD Invalid command\r\n"; 90 | 91 | // The client will need the tag in the response in order to match up 92 | // the response to the command it issued because the client does not 93 | // have to wait on our response in order to issue new commands. 94 | let mut starttls = false; 95 | let res = match args.next() { 96 | None => inv_str.to_string(), 97 | Some(tag) => { 98 | let mut bad_res = tag.to_string(); 99 | bad_res.push_str(inv_str); 100 | 101 | // Interpret the command and generate a response 102 | match args.next() { 103 | None => bad_res, 104 | Some(c) => { 105 | warn!("Cmd: {}", command.trim()); 106 | match &c.to_ascii_lowercase()[..] { 107 | // STARTTLS is handled here because it modifies the stream 108 | "starttls" => { 109 | match stream.get_ref() { 110 | &Stream::Tcp(_) => 111 | if self.serv.can_starttls() { 112 | starttls = true; 113 | let mut ok_res = tag.to_string(); 114 | ok_res.push_str(" OK Begin TLS negotiation now\r\n"); 115 | ok_res 116 | } else { 117 | bad_res 118 | }, 119 | _ => bad_res 120 | } 121 | }, 122 | cmd => self.interpret(cmd, &mut args, tag, bad_res) 123 | } 124 | } 125 | } 126 | } 127 | }; 128 | 129 | // Log the response 130 | warn!("Response:\n{}", res); 131 | 132 | return_on_err!(stream.write(res.as_bytes())); 133 | return_on_err!(stream.flush()); 134 | 135 | if starttls { 136 | if let Some(ssl_stream) = self.serv.starttls(stream.into_inner()) { 137 | stream = BufStream::new(Stream::Ssl(ssl_stream)); 138 | } else { 139 | return; 140 | } 141 | } 142 | 143 | // Exit if the client is logging out, per RFC 3501 144 | if self.logout { 145 | return; 146 | } 147 | } 148 | 149 | // If there is an error on the stream, exit. 150 | Err(_) => { return; } 151 | } 152 | } 153 | } 154 | 155 | /// Interprets a client command and generates a String response 156 | fn interpret(&mut self, cmd: &str, args: &mut Split, 157 | tag: &str, bad_res: String) -> String { 158 | // The argument after the tag specified the command issued. 159 | // Additional arguments are arguments for that specific command. 160 | match cmd { 161 | "noop" => { 162 | let mut res = tag.to_string(); 163 | res += " OK NOOP\r\n"; 164 | res 165 | } 166 | 167 | // Inform the client of the supported IMAP version and 168 | // extension(s) 169 | "capability" => { 170 | let mut res = "* CAPABILITY IMAP4rev1 CHILDREN\r\n" 171 | .to_string(); 172 | res.push_str(tag); 173 | res.push_str(" OK Capability successful\r\n"); 174 | res 175 | } 176 | "login" => { 177 | let login_args: Vec<&str> = args.collect(); 178 | if login_args.len() < 2 { return bad_res; } 179 | let email = login_args[0].trim_matches('"'); 180 | let password = login_args[1].trim_matches('"'); 181 | let mut no_res = tag.to_string(); 182 | no_res.push_str(" NO invalid username or password\r\n"); 183 | if let Some(user) = self.serv.login(email.to_string(), password.to_string()) { 184 | self.maildir = Some(user.maildir.clone()); 185 | } else { 186 | return no_res; 187 | } 188 | match self.maildir { 189 | Some(_) => { 190 | let mut res = tag.to_string(); 191 | res.push_str(" OK logged in successfully as "); 192 | res.push_str(email); 193 | res.push_str("\r\n"); 194 | res 195 | } 196 | None => no_res 197 | } 198 | } 199 | "logout" => { 200 | // Close the connection after sending the response 201 | self.logout = true; 202 | 203 | // Write out current state of selected folder (if any) 204 | // to disk 205 | if let Some(ref folder) = self.folder { 206 | folder.expunge(); 207 | } 208 | 209 | let mut res = "* BYE Server logging out\r\n" 210 | .to_string(); 211 | res.push_str(tag); 212 | res.push_str(" OK Server logged out\r\n"); 213 | res 214 | } 215 | // Examine and Select should be nearly identical... 216 | "select" => { 217 | let maildir = match self.maildir { 218 | None => { return bad_res; } 219 | Some(ref maildir) => maildir 220 | }; 221 | let (folder, res) = util::perform_select(&maildir[..], 222 | &args.collect::>(), 223 | false, tag); 224 | self.folder = folder; 225 | match self.folder { 226 | None => bad_res, 227 | _ => res 228 | } 229 | } 230 | "examine" => { 231 | let maildir = match self.maildir { 232 | None => { return bad_res; } 233 | Some(ref maildir) => maildir 234 | }; 235 | let (folder, res) = util::perform_select(&maildir[..], 236 | &args.collect::>(), 237 | true, tag); 238 | self.folder = folder; 239 | match self.folder { 240 | None => bad_res, 241 | _ => res 242 | } 243 | } 244 | "create" => { 245 | let create_args: Vec<&str> = args.collect(); 246 | if create_args.len() < 1 { return bad_res; } 247 | let mbox_name = create_args[0].trim_matches('"').replace("INBOX", ""); 248 | match self.maildir { 249 | None => bad_res, 250 | Some(ref maildir) => { 251 | let mut no_res = tag.to_string(); 252 | no_res.push_str(" NO Could not create folder.\r\n"); 253 | let maildir_path = Path::new(&maildir[..]).join(mbox_name); 254 | 255 | // Create directory for new mail 256 | let newmaildir_path = maildir_path.join("new"); 257 | if fs::create_dir_all(&newmaildir_path).is_err() { 258 | return no_res; 259 | } 260 | if fs::set_permissions(&newmaildir_path, 261 | fs::Permissions::from_mode(0o755)).is_err() { 262 | return no_res; 263 | } 264 | 265 | // Create directory for current mail 266 | let curmaildir_path = maildir_path.join("cur"); 267 | if fs::create_dir_all(&curmaildir_path).is_err() { 268 | return no_res; 269 | } 270 | if fs::set_permissions(&curmaildir_path, 271 | fs::Permissions::from_mode(0o755)).is_err() { 272 | return no_res; 273 | } 274 | 275 | let mut ok_res = tag.to_string(); 276 | ok_res.push_str(" OK CREATE successful.\r\n"); 277 | ok_res 278 | } 279 | } 280 | } 281 | "delete" => { 282 | let delete_args: Vec<&str> = args.collect(); 283 | if delete_args.len() < 1 { return bad_res; } 284 | let mbox_name = delete_args[0].trim_matches('"').replace("INBOX", ""); 285 | match self.maildir { 286 | None => bad_res, 287 | Some(ref maildir) => { 288 | let mut no_res = tag.to_string(); 289 | no_res.push_str(" NO Invalid folder.\r\n"); 290 | let maildir_path = Path::new(&maildir[..]).join(mbox_name); 291 | let newmaildir_path = maildir_path.join("new"); 292 | let curmaildir_path = maildir_path.join("cur"); 293 | opendirlisting!(&newmaildir_path, newlist, 294 | no_res, 295 | opendirlisting!(&curmaildir_path, curlist, 296 | no_res, 297 | { 298 | // Delete the mail in the folder 299 | for file_entry in newlist { 300 | match file_entry { 301 | Ok(file) => { 302 | if fs::remove_file(file.path()).is_err() { 303 | return no_res; 304 | } 305 | } 306 | Err(_) => return no_res 307 | } 308 | } 309 | for file_entry in curlist { 310 | match file_entry { 311 | Ok(file) => { 312 | if fs::remove_file(file.path()).is_err() { 313 | return no_res; 314 | } 315 | } 316 | Err(_) => return no_res 317 | } 318 | } 319 | 320 | // Delete the folders holding the mail 321 | if fs::remove_dir(&newmaildir_path).is_err() { 322 | return no_res; 323 | } 324 | if fs::remove_dir(&curmaildir_path).is_err() { 325 | return no_res; 326 | } 327 | 328 | // This folder might contain subfolders 329 | // holding mail. For this reason, we 330 | // leave the other files, and the 331 | // folder itself, in tact. 332 | let mut ok_res = tag.to_string(); 333 | ok_res.push_str(" OK DELETE successsful.\r\n"); 334 | ok_res 335 | }) 336 | ) 337 | } 338 | } 339 | } 340 | // List folders which match the specified regular expression. 341 | "list" => { 342 | let list_args: Vec<&str> = args.collect(); 343 | if list_args.len() < 2 { return bad_res; } 344 | let reference = list_args[0].trim_matches('"'); 345 | let mailbox_name = list_args[1].trim_matches('"'); 346 | match self.maildir { 347 | None => bad_res, 348 | Some(ref maildir) => { 349 | if mailbox_name.is_empty() { 350 | return format!("* LIST (\\Noselect) \"/\" \"{}\"\r\n{} OK List successful\r\n", 351 | reference, tag); 352 | } 353 | let mailbox_name = mailbox_name 354 | .replace("*", ".*") 355 | .replace("%", "[^/]*"); 356 | let maildir_path = Path::new(&maildir[..]); 357 | let re_opt = Regex::new 358 | (&format! 359 | ("{}{}?{}{}?{}$", 360 | path_filename_to_str!(maildir_path), 361 | MAIN_SEPARATOR, reference, 362 | MAIN_SEPARATOR, 363 | mailbox_name.replace("INBOX", ""))[..]); 364 | match re_opt { 365 | Err(_) => bad_res, 366 | Ok(re) => { 367 | let list_responses = util::list(&maildir[..], 368 | &re); 369 | let mut ok_res = String::new(); 370 | for list_response in &list_responses { 371 | ok_res.push_str(&list_response[..]); 372 | ok_res.push_str("\r\n"); 373 | } 374 | ok_res.push_str(tag); 375 | ok_res.push_str(" OK list successful\r\n"); 376 | ok_res 377 | } 378 | } 379 | } 380 | } 381 | } 382 | // Resolve state of folder in memory with state of mail on 383 | // disk 384 | "check" => { 385 | match self.expunge() { 386 | _ => {} 387 | } 388 | match self.folder { 389 | None => bad_res, 390 | Some(ref mut folder) => { 391 | folder.check(); 392 | let mut ok_res = tag.to_string(); 393 | ok_res.push_str(" OK Check completed\r\n"); 394 | ok_res 395 | } 396 | } 397 | } 398 | // Close the currently selected folder. Perform all 399 | // required cleanup. 400 | "close" => { 401 | match self.expunge() { 402 | Err(_) => bad_res, 403 | Ok(_) => { 404 | if let Some(ref mut folder) = self.folder { 405 | folder.check(); 406 | } 407 | self.folder = None; 408 | format!("{} OK close completed\r\n", tag) 409 | } 410 | } 411 | } 412 | // Delete the messages currently marked for deletion. 413 | "expunge" => { 414 | match self.expunge() { 415 | Err(_) => bad_res, 416 | Ok(v) => { 417 | let mut ok_res = String::new(); 418 | for i in &v { 419 | ok_res.push_str("* "); 420 | ok_res.push_str(&i.to_string()[..]); 421 | ok_res.push_str(" EXPUNGE\r\n"); 422 | } 423 | ok_res.push_str(tag); 424 | ok_res.push_str(" OK expunge completed\r\n"); 425 | ok_res 426 | } 427 | } 428 | } 429 | "fetch" => { 430 | // Retrieve the current folder, if it exists. 431 | // If it doesn't, the command is invalid. 432 | let folder = match self.folder { 433 | Some(ref mut folder) => folder, 434 | None => return bad_res 435 | }; 436 | 437 | // Parse command, make sure it is validly formed. 438 | let parsed_cmd = match fetch::fetch(args.collect()) { 439 | Ok(cmd) => cmd, 440 | _ => return bad_res 441 | }; 442 | 443 | /* 444 | * Verify that the requested sequence set is valid. 445 | * 446 | * Per RFC 3501 seq-number definition: 447 | * "The server should respond with a tagged BAD 448 | * response to a command that uses a message 449 | * sequence number greater than the number of 450 | * messages in the selected mailbox. This 451 | * includes "*" if the selected mailbox is empty." 452 | */ 453 | let sequence_iter = sequence_set::iterator 454 | (&parsed_cmd.sequence_set, 455 | folder.message_count()); 456 | if sequence_iter.is_empty() { return bad_res } 457 | fetch::fetch_loop(&parsed_cmd, folder, 458 | &sequence_iter, tag, 459 | false) 460 | }, 461 | // These commands use UIDs instead of sequence numbers. 462 | // Sequence numbers map onto the list of messages in the 463 | // folder directly and change whenever messages are added 464 | // or removed from the folder. 465 | "uid" => { 466 | match args.next() { 467 | Some(uidcmd) => { 468 | match &uidcmd.to_ascii_lowercase()[..] { 469 | "fetch" => { 470 | // Retrieve the current folder, if it 471 | // exists. 472 | let folder = match self.folder { 473 | Some(ref mut folder) => folder, 474 | None => return bad_res 475 | }; 476 | // Parse the command with the PEG 477 | // parser. 478 | let mut parsed_cmd = match fetch::fetch(args.collect()) { 479 | Ok(cmd) => cmd, 480 | _ => return bad_res 481 | }; 482 | parsed_cmd.attributes.push(UID); 483 | 484 | // SPECIAL CASE FOR RANGES WITH WILDCARDS 485 | if let Range(ref a, ref b) = parsed_cmd.sequence_set[0] { 486 | if let Number(n) = **a { 487 | if let Wildcard = **b { 488 | if folder.message_count() == 0 { return bad_res } 489 | let start = match folder.get_index_from_uid(&n) { 490 | Some(start) => *start, 491 | None => { 492 | if n == 1 { 493 | 0usize 494 | } else { 495 | return bad_res; 496 | } 497 | } 498 | }; 499 | let mut res = String::new(); 500 | for index in start..folder.message_count() { 501 | res.push_str(&folder.fetch(index+1, &parsed_cmd.attributes)[..]); 502 | } 503 | res.push_str(tag); 504 | res.push_str(" OK UID FETCH completed\r\n"); 505 | return res 506 | } 507 | } 508 | }; 509 | 510 | /* 511 | * Verify that the requested sequence set is valid. 512 | * 513 | * Per RFC 3501 seq-number definition: 514 | * "The server should respond with a tagged BAD 515 | * response to a command that uses a message 516 | * sequence number greater than the number of 517 | * messages in the selected mailbox. This 518 | * includes "*" if the selected mailbox is empty." 519 | */ 520 | let sequence_iter = sequence_set::uid_iterator(&parsed_cmd.sequence_set); 521 | if sequence_iter.is_empty() { return bad_res; } 522 | fetch::fetch_loop(&parsed_cmd, folder, &sequence_iter, tag, true) 523 | } 524 | "store" => { 525 | // There should be a folder selected. 526 | let folder = match self.folder { 527 | None => return bad_res, 528 | Some(ref mut folder) => folder 529 | }; 530 | 531 | match store::store(folder, &args.collect::>(), 532 | true, tag) { 533 | Some(res) => res, 534 | _ => bad_res 535 | } 536 | } 537 | _ => bad_res 538 | } 539 | } 540 | None => bad_res 541 | } 542 | }, 543 | "store" => { 544 | // There should be a folder selected. 545 | let folder = match self.folder { 546 | None => { return bad_res; } 547 | Some(ref mut folder) => folder 548 | }; 549 | 550 | match store::store(folder, &args.collect::>(), false, tag) { 551 | Some(res) => res, 552 | _ => bad_res 553 | } 554 | } 555 | _ => bad_res 556 | } 557 | } 558 | 559 | // should generate list of sequence numbers that were deleted 560 | fn expunge(&self) -> Result, Error> { 561 | match self.folder { 562 | None => { 563 | Err(Error::InvalidImapState) 564 | } 565 | Some(ref folder) => { 566 | Ok(folder.expunge()) 567 | } 568 | } 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /core/src/server/lmtp.rs: -------------------------------------------------------------------------------- 1 | use std::ascii::AsciiExt; 2 | use std::fs::File; 3 | use std::io::{BufRead, Write}; 4 | use std::io::ErrorKind::AlreadyExists; 5 | use std::net::TcpStream; 6 | use std::path::Path; 7 | use std::sync::Arc; 8 | 9 | use bufstream::BufStream; 10 | use num::ToPrimitive; 11 | use time; 12 | 13 | use server::Server; 14 | use server::user::{Email, User}; 15 | 16 | // Just bail if there is some error. 17 | // Used when performing operations on a TCP Stream generally 18 | #[macro_export] 19 | macro_rules! return_on_err( 20 | ($inp:expr) => { 21 | if $inp.is_err() { 22 | return; 23 | } 24 | } 25 | ); 26 | 27 | macro_rules! delivery_ioerror( 28 | ($res:ident) => ({ 29 | $res.push_str("451 Error in processing.\r\n"); 30 | break; 31 | }) 32 | ); 33 | 34 | macro_rules! grab_email_token( 35 | ($arg:expr) => { 36 | match $arg { 37 | Some(from_path) => from_path.trim_left_matches('<').trim_right_matches('>'), 38 | _ => { return None; } 39 | } 40 | } 41 | ); 42 | 43 | struct Lmtp<'a> { 44 | rev_path: Option, 45 | to_path: Vec<&'a User>, 46 | data: String, 47 | quit: bool 48 | } 49 | 50 | static OK: &'static str = "250 OK\r\n"; 51 | 52 | impl<'a> Lmtp<'a> { 53 | fn deliver(&self) -> String { 54 | if self.to_path.is_empty() { 55 | return "503 Bad sequence - no recipients".to_string(); 56 | } 57 | let mut res = String::new(); 58 | for rcpt in &self.to_path { 59 | let mut timestamp = match time::get_time().sec.to_i32() { 60 | Some(i) => i, 61 | None => { 62 | res.push_str("555 Unix 2038 error\r\n"); 63 | break; 64 | } 65 | }; 66 | let maildir = rcpt.maildir.clone(); 67 | let newdir_path = Path::new(&maildir[..]).join("new"); 68 | loop { 69 | match File::create(&newdir_path.join(timestamp.to_string())) { 70 | Err(e) => { 71 | if e.kind() == AlreadyExists { 72 | timestamp += 1; 73 | } else { 74 | delivery_ioerror!(res); 75 | } 76 | } 77 | Ok(mut file) => { 78 | if file.write(self.data.as_bytes()).is_err() { 79 | delivery_ioerror!(res); 80 | } 81 | if file.flush().is_err() { 82 | delivery_ioerror!(res); 83 | } 84 | res.push_str("250 OK\r\n"); 85 | break; 86 | } 87 | } 88 | } 89 | } 90 | res 91 | } 92 | } 93 | 94 | fn grab_email(arg: Option<&str>) -> Option { 95 | let from_path_split = match arg { 96 | Some(full_from_path) => { 97 | let mut split_arg = full_from_path.split(':'); 98 | match split_arg.next() { 99 | Some(from_str) => { 100 | match &from_str.to_ascii_lowercase()[..] { 101 | "from" | "to" => { 102 | grab_email_token!(split_arg.next()) 103 | }, 104 | _ => { return None; } 105 | } 106 | } 107 | _ => { return None; } 108 | } 109 | } 110 | _ => { return None; } 111 | }; 112 | let mut from_parts = from_path_split.split('@'); 113 | let local_part = match from_parts.next() { 114 | Some(part) => part.to_string(), 115 | _ => { return None; } 116 | }; 117 | let domain_part = match from_parts.next() { 118 | Some(part) => part.to_string(), 119 | _ => { return None; } 120 | }; 121 | Some(Email::new(local_part, domain_part)) 122 | } 123 | 124 | pub fn serve(serv: Arc, mut stream: BufStream) { 125 | let mut l = Lmtp { 126 | rev_path: None, 127 | to_path: Vec::new(), 128 | data: String::new(), 129 | quit: false 130 | }; 131 | return_on_err!(stream.write(format!("220 {} LMTP server ready\r\n", 132 | *serv.host()).as_bytes())); 133 | return_on_err!(stream.flush()); 134 | loop { 135 | let mut command = String::new(); 136 | match stream.read_line(&mut command) { 137 | Ok(_) => { 138 | if command.is_empty() { 139 | return; 140 | } 141 | let trimmed_command = (&command[..]).trim(); 142 | let mut args = trimmed_command.split(' '); 143 | let invalid = "500 Invalid command\r\n".to_string(); 144 | let no_such_user = "550 No such user".to_string(); 145 | let data_res = b"354 Start mail input; end with ."; 146 | let ok_res = OK.to_string(); 147 | let res = match args.next() { 148 | Some(cmd) => { 149 | warn!("LMTP Cmd: {}", trimmed_command); 150 | match &cmd.to_ascii_lowercase()[..] { 151 | "lhlo" => { 152 | match args.next() { 153 | Some(domain) => { 154 | format!("250 {}\r\n", domain) 155 | } 156 | _ => invalid 157 | } 158 | } 159 | "rset" => { 160 | l.rev_path = None; 161 | l.to_path = Vec::new(); 162 | ok_res 163 | } 164 | "noop" => ok_res, 165 | "quit" => { 166 | l.quit = true; 167 | format!("221 {} Closing connection\r\n", 168 | *serv.host()) 169 | } 170 | "vrfy" => { 171 | invalid 172 | } 173 | "mail" => { 174 | match grab_email(args.next()) { 175 | None => invalid, 176 | s => { 177 | l.rev_path = s; 178 | ok_res 179 | } 180 | } 181 | } 182 | "rcpt" => { 183 | match l.rev_path { 184 | None => invalid, 185 | _ => { 186 | match grab_email(args.next()) { 187 | None => invalid, 188 | Some(email) => { 189 | match serv.users.get(&email) { 190 | None => no_such_user, 191 | Some(user) => { 192 | l.to_path.push(user); 193 | ok_res 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | "data" => { 202 | return_on_err!(stream.write(data_res)); 203 | return_on_err!(stream.flush()); 204 | let mut loop_res = invalid; 205 | loop { 206 | let mut data_command = String::new(); 207 | match stream.read_line(&mut data_command) { 208 | Ok(_) => { 209 | if data_command.is_empty() { 210 | break; 211 | } 212 | let data_cmd = (&data_command[..]).trim(); 213 | if data_cmd == "." { 214 | loop_res = l.deliver(); 215 | l.data = String::new(); 216 | break; 217 | } 218 | l.data.push_str(data_cmd); 219 | l.data.push('\n'); 220 | } 221 | _ => { break; } 222 | } 223 | } 224 | loop_res 225 | } 226 | _ => invalid 227 | } 228 | } 229 | None => invalid 230 | }; 231 | return_on_err!(stream.write(res.as_bytes())); 232 | return_on_err!(stream.flush()); 233 | if l.quit { 234 | return; 235 | } 236 | } 237 | _ => { break; } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /core/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{Read, Result, Write}; 3 | use std::net::{Shutdown, TcpListener, TcpStream}; 4 | use std::result::Result as StdResult; 5 | use std::sync::Arc; 6 | 7 | use bufstream::{BufStream, IntoInnerError}; 8 | use openssl::ssl::{SslAcceptor, SslStream}; 9 | 10 | use error::ImapResult; 11 | use self::config::Config; 12 | use self::imap::ImapSession; 13 | use self::user::{load_users, Email, LoginData, User}; 14 | 15 | mod config; 16 | #[macro_use] 17 | pub mod lmtp; 18 | mod imap; 19 | mod user; 20 | 21 | pub enum Stream { 22 | Ssl(SslStream), 23 | Tcp(TcpStream) 24 | } 25 | 26 | impl Write for Stream { 27 | fn write(&mut self, buf: &[u8]) -> Result { 28 | match *self { 29 | Stream::Ssl(ref mut s) => s.write(buf), 30 | Stream::Tcp(ref mut s) => s.write(buf) 31 | } 32 | } 33 | 34 | fn flush(&mut self) -> Result<()> { 35 | match *self { 36 | Stream::Ssl(ref mut s) => s.flush(), 37 | Stream::Tcp(ref mut s) => s.flush() 38 | } 39 | } 40 | } 41 | 42 | impl Read for Stream { 43 | fn read(&mut self, buf: &mut [u8]) -> Result { 44 | match *self { 45 | Stream::Ssl(ref mut s) => s.read(buf), 46 | Stream::Tcp(ref mut s) => s.read(buf) 47 | } 48 | } 49 | } 50 | 51 | /// Holds configuration state and email->user map 52 | pub struct Server { 53 | conf: Config, 54 | users: HashMap, 55 | ssl_acceptor: Option, 56 | } 57 | 58 | impl Server { 59 | pub fn new() -> ImapResult { 60 | Server::new_with_conf(Config::new()?) 61 | } 62 | 63 | /// Create server to hold the Config and User HashMap 64 | fn new_with_conf(conf: Config) -> ImapResult { 65 | // Load the user data from the specified user data file. 66 | let users = load_users(&conf.users)?; 67 | let ssl_acceptor = conf.get_ssl_acceptor().ok(); 68 | 69 | Ok(Server { 70 | conf: conf, 71 | users: users, 72 | ssl_acceptor: ssl_acceptor, 73 | }) 74 | } 75 | 76 | /// Create a TCP listener on the server host and input port 77 | fn generic_listener(&self, port_opt: Option) -> Option> { 78 | if let Some(port) = port_opt { 79 | Some(TcpListener::bind((&self.conf.host[..], port))) 80 | } else { 81 | None 82 | } 83 | } 84 | 85 | /// Create a TCP listener on the server host and imap port 86 | pub fn imap_listener(&self) -> Option> { 87 | self.generic_listener(self.conf.imap_port) 88 | } 89 | 90 | /// Create a TCP listener on the server host and imap ssl port 91 | pub fn imap_ssl_listener(&self) -> Option> { 92 | self.generic_listener(self.conf.imap_ssl_port) 93 | } 94 | 95 | /// Create a TCP listener on the server host and lmtp port 96 | pub fn lmtp_listener(&self) -> Option> { 97 | self.generic_listener(self.conf.lmtp_port) 98 | } 99 | 100 | /// Create a TCP listener on the server host and lmtp ssl port 101 | pub fn lmtp_ssl_listener(&self) -> Option> { 102 | self.generic_listener(self.conf.lmtp_ssl_port) 103 | } 104 | 105 | pub fn imap_ssl(&self, stream: TcpStream) -> Stream { 106 | if let Ok(addr) = stream.local_addr() { 107 | if Some(addr.port()) == self.conf.imap_ssl_port { 108 | if let Some(ref ssl_acceptor) = self.ssl_acceptor { 109 | return Stream::Ssl(ssl_acceptor.accept(stream).unwrap()); 110 | } 111 | error!("Listening on SSL port without SSL certificate configured."); 112 | let _ = stream.shutdown(Shutdown::Both); 113 | } 114 | } 115 | Stream::Tcp(stream) 116 | } 117 | 118 | pub fn can_starttls(&self) -> bool { 119 | if let Some(_) = self.ssl_acceptor { 120 | true 121 | } else { 122 | false 123 | } 124 | } 125 | 126 | pub fn starttls(&self, inner_stream: StdResult>>) -> Option> { 127 | if let Ok(Stream::Tcp(stream)) = inner_stream { 128 | if let Some(ref ssl_acceptor) = self.ssl_acceptor { 129 | if let Ok(ssl_stream) = ssl_acceptor.accept(stream) { 130 | return Some(ssl_stream); 131 | } 132 | } 133 | } 134 | None 135 | } 136 | 137 | fn host(&self) -> &String { 138 | &self.conf.host 139 | } 140 | 141 | pub fn login(&self, email: String, password: String) -> Option<&User> { 142 | if let Some(login_data) = LoginData::new(email, password) { 143 | if let Some(user) = self.users.get(&login_data.email) { 144 | if user.auth_data.verify_auth(login_data.password) { 145 | return Some(user); 146 | } 147 | } 148 | } 149 | None 150 | } 151 | } 152 | 153 | pub fn lmtp_serve(serv: Arc, stream: TcpStream) { 154 | lmtp::serve(serv, BufStream::new(stream)) 155 | } 156 | 157 | pub fn imap_serve(serv: Arc, stream: TcpStream) { 158 | let mut session = ImapSession::new(serv); 159 | session.handle(stream); 160 | } 161 | -------------------------------------------------------------------------------- /core/src/server/user/auth.rs: -------------------------------------------------------------------------------- 1 | // Use OsRng to ensure that the randomly generated data is cryptographically 2 | // secure. 3 | use rand::Rng; 4 | use rand::os::OsRng; 5 | 6 | // Use bcrypt for the hashing algorithm to ensure that the outputted data is 7 | // cryptograpically secure and difficult to crack, even if the authentication 8 | // database is leaked. 9 | use crypto::bcrypt_pbkdf::bcrypt_pbkdf; 10 | 11 | /// The number of rounds of bcrypt hashing to apply to the password. 12 | static ROUNDS: u32 = 10; 13 | 14 | /// Secure representation of the user's password 15 | #[derive(Debug, Deserialize, Serialize)] 16 | pub struct AuthData { 17 | /// Added to the password before hashing 18 | salt: Vec, 19 | /// The hash of the password 20 | out: Vec 21 | } 22 | 23 | impl AuthData { 24 | /// Generates a hash and salt for secure storage of a password 25 | pub fn new(password: String) -> AuthData { 26 | let salt = gen_salt(); 27 | let password = password.into_bytes(); 28 | // Perform the bcrypt hashing, storing it to an output vector. 29 | let out = &mut [0u8; 32]; 30 | bcrypt_pbkdf(&password[..], &salt[..], ROUNDS, out); 31 | 32 | AuthData { 33 | salt: salt, 34 | out: out.to_vec() 35 | } 36 | } 37 | 38 | /// Verify a password string against the stored auth data to see if it 39 | /// matches. 40 | pub fn verify_auth(&self, password: String) -> bool { 41 | let out = &mut [0u8; 32]; 42 | bcrypt_pbkdf( 43 | &password.into_bytes()[..], 44 | &self.salt[..], 45 | ROUNDS, 46 | out); 47 | self.out == out.to_vec() 48 | } 49 | } 50 | 51 | /// Generate a random salt using the cryptographically secure PRNG provided by 52 | /// the OS, for use with bcrypt hashing. 53 | fn gen_salt() -> Vec { 54 | // Use the cryptographically secure OsRng for randomness. 55 | let mut rng = match OsRng::new() { 56 | Ok(v) => v, 57 | Err(e) => panic!("Failed to create secure Rng: {}", e) 58 | }; 59 | // Generate the salt from a set of random ascii characters. 60 | let mut salt = String::new(); 61 | for n in rng.gen_ascii_chars().take(16) { 62 | salt.push(n); 63 | } 64 | // Convert the salt into bytes for hashing. 65 | salt.into_bytes() 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use user::auth; 71 | 72 | #[test] 73 | fn test_valid_auth_data() { 74 | let auth_data = auth::AuthData::new("12345".to_string()); 75 | assert!(auth_data.verify_auth("12345".to_string())); 76 | } 77 | 78 | #[test] 79 | fn test_invalid_auth_data() { 80 | let auth_data = auth::AuthData::new("12345".to_string()); 81 | assert!(!auth_data.verify_auth("54321".to_string())); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/src/server/user/email.rs: -------------------------------------------------------------------------------- 1 | /// Representation of an email 2 | /// This helps ensure the email at least has an '@' in it... 3 | #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] 4 | pub struct Email { 5 | pub local_part: String, 6 | pub domain_part: String 7 | } 8 | 9 | impl Email { 10 | pub fn new(local_part: String, domain_part: String) -> Email { 11 | Email { 12 | local_part: local_part, 13 | domain_part: domain_part 14 | } 15 | } 16 | 17 | #[allow(dead_code)] 18 | fn to_string(&self) -> String { 19 | let mut res = self.local_part.clone(); 20 | res.push('@'); 21 | res.push_str(&self.domain_part[..]); 22 | res 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/server/user/login.rs: -------------------------------------------------------------------------------- 1 | use super::email::Email; 2 | 3 | /// Representation of an email and password login attempt. 4 | pub struct LoginData { 5 | pub email: Email, 6 | pub password: String 7 | } 8 | 9 | impl LoginData { 10 | pub fn new(email: String, password: String) -> Option { 11 | let mut parts = (&email[..]).split('@'); 12 | if let Some(local_part) = parts.next() { 13 | if let Some(domain_part) = parts.next() { 14 | let login_data = LoginData { 15 | email: Email { 16 | local_part: local_part.to_string(), 17 | domain_part: domain_part.to_string() 18 | }, 19 | password: password 20 | }; 21 | return Some(login_data); 22 | } 23 | } 24 | 25 | None 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/server/user/mod.rs: -------------------------------------------------------------------------------- 1 | use error::ImapResult; 2 | use self::auth::AuthData; 3 | use serde_json; 4 | use std::collections::HashMap; 5 | use std::fs::File; 6 | use std::io::{Read, Write}; 7 | use std::path::Path; 8 | use std::str; 9 | 10 | pub use self::email::Email; 11 | pub use self::login::LoginData; 12 | 13 | mod auth; 14 | mod email; 15 | mod login; 16 | 17 | /// Representation of a User. 18 | #[derive(Debug, Deserialize, Serialize)] 19 | pub struct User { 20 | /// The email address through which the user logs in. 21 | pub email: Email, 22 | /// The authentication data the used to verify the user's identity. 23 | pub auth_data: AuthData, 24 | /// The root directory in which the user's mail is stored. 25 | pub maildir: String 26 | } 27 | 28 | impl User { 29 | /// Creates a new user from a provided email, plaintext password, and root 30 | /// mail directory. 31 | pub fn new(email: Email, password: String, maildir: String) -> User { 32 | User { 33 | email: email, 34 | auth_data: AuthData::new(password), 35 | maildir: maildir 36 | } 37 | } 38 | } 39 | 40 | /// Reads a JSON file and turns it into a `HashMap` of emails to users. 41 | /// May throw an `std::io::Error`, hence the `Result<>` type. 42 | pub fn load_users(path_str: &str) -> ImapResult> { 43 | let path = Path::new(&path_str[..]); 44 | 45 | let users = match File::open(&path) { 46 | Ok(mut file) => { 47 | let mut file_buf: String = String::new(); 48 | file.read_to_string(&mut file_buf)?; 49 | serde_json::from_str(&file_buf)? 50 | }, 51 | Err(e) => { 52 | warn!("Failed to open users file, creating default: {}", e); 53 | create_default_users(&path)? 54 | } 55 | }; 56 | 57 | let mut map = HashMap::::new(); 58 | for user in users { 59 | map.insert(user.email.clone(), user); 60 | } 61 | 62 | Ok(map) 63 | } 64 | 65 | /// Writes a list of users to a new file on the disk. 66 | pub fn save_users(path: &Path, users: &[User]) -> ImapResult<()> { 67 | let encoded = serde_json::to_string(&users)?; 68 | 69 | let mut file = File::create(&path)?; 70 | file.write(encoded.as_bytes())?; 71 | 72 | Ok(()) 73 | } 74 | 75 | /// Function to create an example users JSON file at the specified path. 76 | /// 77 | /// Returns the list of example users. 78 | fn create_default_users(path: &Path) -> ImapResult> { 79 | let users = vec![ 80 | User::new( 81 | Email::new("will".to_string(), "xqz.ca".to_string()), 82 | "54321".to_string(), 83 | "./maildir".to_string() 84 | ), 85 | User::new( 86 | Email::new("nikitapekin".to_string(), "gmail.com".to_string()), 87 | "12345".to_string(), 88 | "./maildir".to_string() 89 | ), 90 | ]; 91 | 92 | save_users(path, &users)?; 93 | 94 | Ok(users) 95 | } 96 | -------------------------------------------------------------------------------- /core/src/util.rs: -------------------------------------------------------------------------------- 1 | // This file is made up largely of utility methods which are invoked by the 2 | // session in its interpret method. They are separate because they don't rely 3 | // on the session (or take what they do need as arguments) and/or they are 4 | // called by the session in multiple places. 5 | 6 | use std::env::current_dir; 7 | use std::fs; 8 | use std::path::Path; 9 | use std::path::PathBuf; 10 | use regex::Regex; 11 | use walkdir::WalkDir; 12 | 13 | use folder::Folder; 14 | 15 | #[macro_export] 16 | macro_rules! path_filename_to_str( 17 | ($p:ident) => ({ 18 | use std::ffi::OsStr; 19 | $p.file_name().unwrap_or_else(|| OsStr::new("")).to_str().unwrap_or_else(|| "") 20 | }); 21 | ); 22 | 23 | fn make_absolute(dir: &Path) -> String { 24 | match current_dir() { 25 | Err(_) => dir.display().to_string(), 26 | Ok(absp) => { 27 | let mut abs_path = absp.clone(); 28 | abs_path.push(dir); 29 | abs_path.display().to_string() 30 | } 31 | } 32 | } 33 | 34 | pub fn perform_select(maildir: &str, select_args: &[&str], examine: bool, 35 | tag: &str) -> (Option, String) { 36 | let err_res = (None, "".to_string()); 37 | if select_args.len() < 1 { return err_res; } 38 | let mbox_name = select_args[0].trim_matches('"').replace("INBOX", "."); 39 | let mut maildir_path = PathBuf::new(); 40 | maildir_path.push(maildir); 41 | maildir_path.push(mbox_name); 42 | let folder = match Folder::new(maildir_path, examine) { 43 | None => { return err_res; } 44 | Some(folder) => folder.clone() 45 | }; 46 | 47 | let ok_res = folder.select_response(tag); 48 | (Some(folder), ok_res) 49 | } 50 | 51 | /// For the given dir, make sure it is a valid mail folder and, if it is, 52 | /// generate the LIST response for it. 53 | fn list_dir(dir: &Path, regex: &Regex, maildir_path: &Path) -> Option { 54 | let dir_string = dir.display().to_string(); 55 | let dir_name = path_filename_to_str!(dir); 56 | 57 | // These folder names are used to hold mail. Every other folder is 58 | // valid. 59 | if dir_name == "cur" || dir_name == "new" || dir_name == "tmp" { 60 | return None; 61 | } 62 | 63 | let abs_dir = make_absolute(dir); 64 | 65 | // If it doesn't have any mail, then it isn't selectable as a mail 66 | // folder but it may contain subfolders which hold mail. 67 | let mut flags = match fs::read_dir(&dir.join("cur")) { 68 | Err(_) => "\\Noselect".to_string(), 69 | _ => { 70 | match fs::read_dir(&dir.join("new")) { 71 | Err(_) => "\\Noselect".to_string(), 72 | // If there is new mail in the folder, we should inform the 73 | // client. We do this only because we have to perform the 74 | // check in order to determine selectability. The RFC says 75 | // not to perform the check if it would slow down the 76 | // response time. 77 | Ok(newlisting) => { 78 | if newlisting.count() == 0 { 79 | "\\Unmarked".to_string() 80 | } else { 81 | "\\Marked".to_string() 82 | } 83 | } 84 | } 85 | } 86 | }; 87 | 88 | // Changing folders in mutt doesn't work properly if we don't indicate 89 | // whether or not a given folder has subfolders. Mutt has issues 90 | // selecting folders with subfolders for reading mail, unfortunately. 91 | match fs::read_dir(&dir) { 92 | Err(_) => { return None; } 93 | Ok(dir_listing) => { 94 | let mut children = false; 95 | for subdir_entry in dir_listing { 96 | if let Ok(subdir) = subdir_entry { 97 | if *dir == *maildir_path { 98 | break; 99 | } 100 | let subdir_path = subdir.path(); 101 | let subdir_str = path_filename_to_str!(subdir_path); 102 | if subdir_str != "cur" && 103 | subdir_str != "new" && 104 | subdir_str != "tmp" { 105 | if fs::read_dir(&subdir.path().join("cur")).is_err() { 106 | continue; 107 | } 108 | if fs::read_dir(&subdir.path().join("new")).is_err() { 109 | continue; 110 | } 111 | children = true; 112 | break; 113 | } 114 | } 115 | } 116 | if children { 117 | flags.push_str(" \\HasChildren"); 118 | } else { 119 | flags.push_str(" \\HasNoChildren"); 120 | } 121 | } 122 | } 123 | 124 | let re_path = make_absolute(maildir_path); 125 | match fs::metadata(dir) { 126 | Err(_) => return None, 127 | Ok(md) => 128 | if !md.is_dir() { 129 | return None; 130 | } 131 | }; 132 | 133 | if !regex.is_match(&dir_string[..]) { 134 | return None; 135 | } 136 | let mut list_str = "* LIST (".to_string(); 137 | list_str.push_str(&flags[..]); 138 | list_str.push_str(") \"/\" "); 139 | let list_dir_string = if abs_dir.starts_with(&re_path[..]) { 140 | abs_dir.replacen(&re_path[..], "", 1) 141 | } else { 142 | abs_dir 143 | }; 144 | list_str.push_str(&(list_dir_string.replace("INBOX", ""))[..]); 145 | Some(list_str) 146 | } 147 | 148 | /// Go through the logged in user's maildir and list every folder matching 149 | /// the given regular expression. Returns a list of LIST responses. 150 | pub fn list(maildir: &str, regex: &Regex) -> Vec { 151 | let maildir_path = Path::new(maildir); 152 | let mut responses = Vec::new(); 153 | if let Some(list_response) = list_dir(maildir_path, regex, maildir_path) { 154 | responses.push(list_response); 155 | } 156 | for dir_res in WalkDir::new(&maildir_path) { 157 | if let Ok(dir) = dir_res { 158 | if let Some(list_response) = list_dir(dir.path(), regex, maildir_path) { 159 | responses.push(list_response); 160 | } 161 | } 162 | } 163 | responses 164 | } 165 | -------------------------------------------------------------------------------- /maildir/cur/1416546579:2,FS: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | X-Original-To: will@localhost 3 | Delivered-To: will@localhost 4 | Received: from coruscant.xqz.ca (localhost [127.0.0.1]) 5 | by j.xqz.ca (Postfix) with ESMTP id A90571172055 6 | for ; Fri, 21 Nov 2014 00:09:39 -0500 (EST) 7 | Delivered-To: wpear089@g.uottawa.ca 8 | Received: from gmail-pop.l.google.com [64.233.181.108] 9 | by coruscant.xqz.ca with POP3 (fetchmail-6.3.21) 10 | for (single-drop); Fri, 21 Nov 2014 00:09:39 -0500 (EST) 11 | Received: by 10.140.95.225 with SMTP id i88csp143193qge; 12 | Thu, 20 Nov 2014 20:20:07 -0800 (PST) 13 | X-Received: by 10.42.167.1 with SMTP id q1mr10890924icy.48.1416543606972; 14 | Thu, 20 Nov 2014 20:20:06 -0800 (PST) 15 | Received: from mx11.uottawa.ca (mx11.uottawa.ca. [137.122.6.149]) 16 | by mx.google.com with ESMTP id y126si3092959iod.20.2014.11.20.20.20.06 17 | for ; 18 | Thu, 20 Nov 2014 20:20:06 -0800 (PST) 19 | Received-SPF: none (google.com: apache@uottawa.ca does not designate permitted sender hosts) client-ip=137.122.6.70; 20 | Authentication-Results: mx.google.com; 21 | spf=none (google.com: apache@uottawa.ca does not designate permitted sender hosts) smtp.mail=apache@uottawa.ca 22 | Received: from localhost (unknown [127.0.0.1]) 23 | by mx11.uottawa.ca (Postfix) with ESMTP id B9FBF110C91 24 | for ; Fri, 21 Nov 2014 04:20:06 +0000 (UTC) 25 | X-Virus-Scanned: amavisd-new at uottawa.ca 26 | Received: from mx4.uottawa.ca ([137.122.6.70]) 27 | by localhost (mx11.uottawa.ca [137.122.6.149]) (amavisd-new, port 10024) 28 | with ESMTP id HW46q0HUoM0Y for ; 29 | Thu, 20 Nov 2014 23:20:06 -0500 (EST) 30 | Received: from igor.dmz.uottawa.ca (unknown [137.122.14.65]) 31 | by mx4.uottawa.ca (Postfix) with ESMTP id A1077E0CBF 32 | for ; Thu, 20 Nov 2014 23:20:06 -0500 (EST) 33 | Received: from dries.dmz.uottawa.ca (dries.dmz.uottawa.ca [172.20.17.8]) 34 | by igor.dmz.uottawa.ca (Postfix) with ESMTP id 9F777C8060 35 | for ; Thu, 20 Nov 2014 23:20:06 -0500 (EST) 36 | Received: from dries.dmz.uottawa.ca (localhost.localdomain [127.0.0.1]) 37 | by dries.dmz.uottawa.ca (8.13.8/8.13.8) with ESMTP id sAL4K6UQ003539 38 | for ; Thu, 20 Nov 2014 23:20:06 -0500 39 | Received: (from apache@localhost) 40 | by dries.dmz.uottawa.ca (8.13.8/8.13.8/Submit) id sAL4K6TX003538; 41 | Thu, 20 Nov 2014 23:20:06 -0500 42 | Date: Thu, 20 Nov 2014 23:20:06 -0500 43 | Message-Id: <201411210420.sAL4K6TX003538@dries.dmz.uottawa.ca> 44 | To: wpear089@uottawa.ca 45 | Subject: TA contact email 46 | MIME-Version: 1.0 47 | Content-Type: text/html; charset=UTF-8; format=flowed; 48 | Content-Transfer-Encoding: 8Bit 49 | X-Mailer: Pressflow 50 | Errors-To: webmaster@uottawa.ca 51 | Sender: webmaster@uottawa.ca 52 | From: lmoura@uottawa.ca 53 | Received-On-Date: Fri Nov 21 00:09:39 EST 2014 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 80 | 81 |
64 | 65 | 66 | 77 | 78 |
67 |

Dear csi2110 students:

68 |

Some students wanted contact of TAs who marked a past assignment because 69 | they had questions about the marking/mistakes.

70 |

I have updated some information at the page below that lists which TA 71 | marks what and including TA contact emails:

72 |

http://www.site.uottawa.ca/~lucia/courses/2110-14/TA-LABDGD.pdf

75 |

regards, Lucia Moura

76 |
79 |
82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /maildir/cur/1416716125: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiri/SEGIMAP/57375149ce1b82ffc1b62d97f74f0c052d59a976/maildir/cur/1416716125 -------------------------------------------------------------------------------- /maildir/new/1414871673: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiri/SEGIMAP/57375149ce1b82ffc1b62d97f74f0c052d59a976/maildir/new/1414871673 -------------------------------------------------------------------------------- /mime/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "segimap_mime" 3 | version = "0.0.1" 4 | 5 | -------------------------------------------------------------------------------- /mime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "segimap_mime" 3 | version = "0.0.1" 4 | authors = [] 5 | 6 | [dependencies] 7 | 8 | [lib] 9 | name = "mime" 10 | -------------------------------------------------------------------------------- /mime/src/command.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Debug)] 2 | pub enum BodySectionType { 3 | AllSection, 4 | MsgtextSection(Msgtext), 5 | PartSection(Vec, Option) 6 | } 7 | 8 | #[derive(PartialEq, Debug)] 9 | pub enum Msgtext { 10 | HeaderMsgtext, 11 | HeaderFieldsMsgtext(Vec), 12 | HeaderFieldsNotMsgtext(Vec), 13 | TextMsgtext, 14 | MimeMsgtext 15 | } 16 | -------------------------------------------------------------------------------- /mime/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | use std::io; 4 | use std::result::Result as StdResult; 5 | 6 | /// A convenient alias type for results for `mime`. 7 | pub type Result = StdResult; 8 | 9 | /// Represents errors which occur during MIME parsing. 10 | #[derive(Debug)] 11 | pub enum Error { 12 | /// An internal `std::io` error. 13 | Io(io::Error), 14 | /// An error occurs when a `Content-Type` is unspecified for a body part. 15 | MissingContentType, 16 | /// An error which occurs when the parser failed to determine the MULTIPART 17 | /// boundary. 18 | ParseMultipartBoundary, 19 | } 20 | 21 | impl fmt::Display for Error { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | use self::Error::*; 24 | 25 | match *self { 26 | MissingContentType | 27 | ParseMultipartBoundary => write!(f, "{}", StdError::description(self)), 28 | Io(ref e) => e.fmt(f), 29 | } 30 | } 31 | } 32 | 33 | impl StdError for Error { 34 | fn description(&self) -> &str { 35 | use self::Error::*; 36 | 37 | match *self { 38 | MissingContentType => "Missing `Content-Type` for body part.", 39 | ParseMultipartBoundary => "Failed to parse MULTIPART boundary.", 40 | Io(ref e) => e.description(), 41 | } 42 | } 43 | 44 | fn cause(&self) -> Option<&StdError> { 45 | use self::Error::*; 46 | 47 | match *self { 48 | ParseMultipartBoundary | 49 | MissingContentType => None, 50 | Io(ref e) => e.cause(), 51 | } 52 | } 53 | } 54 | 55 | // Implement `PartialEq` manually, since `std::io::Error` does not implement it. 56 | impl PartialEq for Error { 57 | fn eq(&self, other: &Error) -> bool { 58 | use self::Error::*; 59 | 60 | match (self, other) { 61 | (&Io(_), &Io(_)) | 62 | (&MissingContentType, &MissingContentType) | 63 | (&ParseMultipartBoundary, &ParseMultipartBoundary) => true, 64 | _ => false, 65 | } 66 | } 67 | } 68 | 69 | impl From for Error { 70 | fn from(error: io::Error) -> Error { 71 | Error::Io(error) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /mime/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ascii::AsciiExt; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::Path; 6 | use std::str; 7 | 8 | pub use self::command::BodySectionType; 9 | use self::command::BodySectionType::{ 10 | AllSection, 11 | MsgtextSection, 12 | PartSection 13 | }; 14 | 15 | pub use self::command::Msgtext; 16 | use self::command::Msgtext::{ 17 | HeaderMsgtext, 18 | HeaderFieldsMsgtext, 19 | HeaderFieldsNotMsgtext, 20 | TextMsgtext, 21 | MimeMsgtext 22 | }; 23 | 24 | pub use self::error::Error; 25 | use self::error::Result as MimeResult; 26 | 27 | mod error; 28 | mod command; 29 | 30 | static RECEIVED: &'static str = "RECEIVED"; 31 | 32 | #[derive(Debug, Clone)] 33 | pub struct Message { 34 | // maps header field names to values 35 | headers: HashMap, 36 | 37 | // contains the MIME Parts (if more than one) of the message 38 | body: Vec, 39 | 40 | // size stored in case FETCH asks for it 41 | size: usize, 42 | 43 | // the raw contents of the file representing the message 44 | raw_contents: String, 45 | 46 | // where in raw_contents the header ends and the body begins 47 | header_boundary: usize 48 | } 49 | 50 | /// Representation of a MIME message part 51 | #[derive(Debug, Clone)] 52 | struct MIMEPart { 53 | mime_header: String, 54 | mime_body: String 55 | } 56 | 57 | impl Message { 58 | pub fn new(arg_path: &Path) -> MimeResult { 59 | // Load the file contents. 60 | let mut file = File::open(arg_path)?; 61 | let mut raw_contents = String::new(); 62 | file.read_to_string(&mut raw_contents)?; 63 | 64 | // This slice will avoid copying later 65 | let size = raw_contents.len(); 66 | 67 | // Find boundary between header and body. 68 | // Use it to create &str of the raw header and raw body 69 | let header_boundary = match raw_contents.find("\n\n") { 70 | None => { return Err(Error::ParseMultipartBoundary); } 71 | Some(n) => n + 1 72 | }; 73 | let raw_header = &raw_contents[ .. header_boundary]; 74 | let raw_body = &raw_contents[header_boundary .. ]; 75 | 76 | // Iterate over the lines of the header in reverse. 77 | // If a line with leading whitespace is detected, it is merged to the 78 | // line before it. 79 | // This "unfolds" the header as indicated in RFC 2822 2.2.3 80 | let mut iterator = raw_header.lines().rev(); 81 | let mut headers = HashMap::new(); 82 | while let Some(line) = iterator.next() { 83 | if line.starts_with(' ') || line.starts_with('\t') { 84 | while let Some(next) = iterator.next() { 85 | let mut trimmed_next = next.trim_left_matches(' ') 86 | .trim_left_matches('\t').to_string(); 87 | 88 | // Add a space between the merged lines. 89 | trimmed_next.push(' '); 90 | trimmed_next.push_str(line.trim_left_matches(' ') 91 | .trim_left_matches('\t')); 92 | if !next.starts_with(' ') && !next.starts_with('\t') { 93 | let split: Vec<&str> = (&trimmed_next[..]) 94 | .splitn(2, ':').collect(); 95 | headers.insert(split[0].to_ascii_uppercase(), 96 | split[1][1 .. ].to_string()); 97 | break; 98 | } 99 | } 100 | } else { 101 | let split: Vec<&str> = line.splitn(2, ':').collect(); 102 | headers.insert(split[0].to_ascii_uppercase(), 103 | split[1][1 .. ].to_string()); 104 | } 105 | } 106 | 107 | // Remove the "Received" key from the HashMap. 108 | let received_key = &RECEIVED.to_string(); 109 | if headers.get(received_key).is_some() { 110 | headers.remove(received_key); 111 | } 112 | 113 | // Determine whether the message is MULTIPART or not. 114 | let mut body = Vec::new(); 115 | match headers.get(&"CONTENT-TYPE".to_string()) { 116 | Some(content_type) => { 117 | if (&content_type[..]).contains("MULTIPART") { 118 | // We need the boundary to determine where this part ends 119 | let mime_boundary = { 120 | let value: Vec<&str> = (&content_type[..]) 121 | .split("BOUNDARY=\"") 122 | .collect(); 123 | if value.len() < 2 { 124 | return Err(Error::ParseMultipartBoundary) 125 | } 126 | let value: Vec<&str> = value[1].splitn(2, '"') 127 | .collect(); 128 | if value.len() < 1 { 129 | return Err(Error::ParseMultipartBoundary) 130 | } 131 | format!("--{}--\n", value[0]) 132 | }; 133 | 134 | // Grab the content type for this part 135 | let first_content_type_index = 136 | match raw_body.find("Content-Type") { 137 | Some(val) => val, 138 | None => return Err(Error::MissingContentType), 139 | }; 140 | let mime_boundary_slice = &mime_boundary[..]; 141 | let raw_body = &raw_body[first_content_type_index .. ]; 142 | let raw_body: Vec<&str> = raw_body.split( 143 | mime_boundary_slice).collect(); 144 | let raw_body_end = raw_body.len() - 1; 145 | let raw_body = &raw_body[ .. raw_body_end]; 146 | 147 | // Throw the parts of the message into a list of MIMEParts 148 | for part in raw_body.iter() { 149 | let header_boundary = match part.find("\n\n") { 150 | None => return Err(Error::ParseMultipartBoundary), 151 | Some(n) => n 152 | }; 153 | let header = &part[ .. header_boundary]; 154 | let mut content_type = String::new(); 155 | for line in header.lines() { 156 | let split_line: Vec<&str> = line.splitn(2, ':') 157 | .collect(); 158 | if split_line[0] == "Content-Type" { 159 | let content_type_values: Vec<&str> = 160 | split_line[1].splitn(2, ';').collect(); 161 | content_type = content_type_values[0][1 .. ].to_string(); 162 | break; 163 | } 164 | } 165 | let body_part = MIMEPart { 166 | mime_header: content_type.to_string(), 167 | // TODO: double check that this is working as 168 | // intended. 169 | mime_body: part.to_string() 170 | }; 171 | body.push(body_part); 172 | } 173 | } else { 174 | // Not a multipart message. 175 | let body_part = MIMEPart { 176 | mime_header: content_type.to_string(), 177 | mime_body: raw_body.to_string() 178 | }; 179 | body.push(body_part); 180 | } 181 | } 182 | // No Content Type header so it is not a MIME message 183 | _ => { 184 | let non_mime_part = MIMEPart { 185 | mime_header: "text/plain".to_string(), 186 | mime_body: raw_body.to_string() 187 | }; 188 | body.push(non_mime_part); 189 | } 190 | } 191 | let message = Message { 192 | headers: headers, 193 | body: body, 194 | size: size, 195 | raw_contents: raw_contents.to_string(), 196 | header_boundary: header_boundary 197 | }; 198 | 199 | // We created the message with no errors. Yay! 200 | Ok(message) 201 | } 202 | 203 | // Both BodyPeek and BodySection grab parts of the message 204 | // BodyPeek does not set the Seen flag while BodySection does. 205 | // Setting the Seen flag is handled in the Session by detecting BodySection 206 | pub fn get_body<'a>(&self, section: &'a BodySectionType, 207 | _octets: &Option<(usize, usize)>) -> String { 208 | let empty_string = "".to_string(); 209 | let peek_attr = match *section { 210 | AllSection => { 211 | format!("] {{{}}}\r\n{} ", (&self.raw_contents[..]).len(), 212 | self.raw_contents) 213 | } 214 | MsgtextSection(ref msgtext) => { 215 | match *msgtext { 216 | HeaderMsgtext | 217 | HeaderFieldsNotMsgtext(_) | 218 | TextMsgtext | 219 | MimeMsgtext => { empty_string }, 220 | HeaderFieldsMsgtext(ref fields) => { 221 | let mut field_keys = String::new(); 222 | let mut field_values = String::new(); 223 | let mut first = true; 224 | for field in fields.iter() { 225 | match self.headers.get(field) { 226 | Some(v) => { 227 | let field_slice = &field[..]; 228 | if first { 229 | first = false; 230 | } else { 231 | field_keys.push(' '); 232 | } 233 | field_keys.push_str(field_slice); 234 | field_values.push_str("\r\n"); 235 | field_values.push_str(field_slice); 236 | field_values.push_str(": "); 237 | field_values.push_str(&v[..]); 238 | }, 239 | None => continue 240 | } 241 | } 242 | format!("HEADER.FIELDS ({})] {{{}}}{}", field_keys, 243 | &field_values[..].len(), field_values) 244 | }, 245 | } 246 | } 247 | PartSection(_, _) => { "?]".to_string() } 248 | }; 249 | format!("BODY[{} ", peek_attr) 250 | } 251 | 252 | /** 253 | * RFC3501 - 7.4.2 - P.76-77 254 | * 255 | * Returns a parenthesized list that described the envelope structure of a 256 | * message. 257 | * Computed by parsing the [RFC-2822] header into the component parts, 258 | * defaulting various fields as necessary. 259 | * 260 | * Requires (in the following order): date, subject, from, sender, 261 | * reply-to, to, cc, bcc, in-reply-to, and message-id. 262 | * The date, subject, in-reply-to, and message-id fields are strings. 263 | * The from, sender, reply-to, to, cc, and bcc fields are parenthesized 264 | * lists of address structures. 265 | */ 266 | pub fn get_envelope(&self) -> String { 267 | let date = self.get_field_or_nil("DATE"); 268 | let subject = self.get_field_or_nil("SUBJECT"); 269 | let from = self.get_parenthesized_addresses("FROM"); 270 | let sender = self.get_parenthesized_addresses("SENDER"); 271 | let reply_to = self.get_parenthesized_addresses("REPLY-TO"); 272 | let to = self.get_parenthesized_addresses("TO"); 273 | let cc = self.get_parenthesized_addresses("CC"); 274 | let bcc = self.get_parenthesized_addresses("BCC"); 275 | let in_reply_to = self.get_field_or_nil("IN-REPLY-TO"); 276 | let message_id = self.get_field_or_nil("MESSAGE-ID"); 277 | 278 | format!( 279 | "(\"{}\" \"{}\" {} {} {} {} {} {} \"{}\" \"{}\")", 280 | date, 281 | subject, 282 | from, 283 | sender, 284 | reply_to, 285 | to, 286 | cc, 287 | bcc, 288 | in_reply_to, 289 | message_id) 290 | } 291 | 292 | pub fn get_field_or_nil(&self, key: &str) -> &str { 293 | match self.headers.get(&key.to_string()) { 294 | Some(v) => &v[..], 295 | None => "NIL" 296 | } 297 | } 298 | 299 | /** 300 | * RFC3501 - 7.4.2 - P.76 301 | * 302 | * The RFC requests that the data be returned as a parenthesized list, but 303 | * the current format is also acceptible by most mail clients. 304 | */ 305 | pub fn get_parenthesized_addresses(&self, key: &str) -> &str { 306 | match self.headers.get(&key.to_string()) { 307 | Some(v) => &v[..], 308 | None => "NIL" 309 | } 310 | } 311 | 312 | pub fn get_size(&self) -> String { 313 | self.size.to_string() 314 | } 315 | 316 | pub fn get_header_boundary(&self) -> String { 317 | self.header_boundary.to_string() 318 | } 319 | 320 | pub fn get_header(&self) -> &str { 321 | &self.raw_contents[ .. self.header_boundary] 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /reset.sh: -------------------------------------------------------------------------------- 1 | mv maildir/cur/1414871673 maildir/new/1414871673 2 | -------------------------------------------------------------------------------- /telnet/telnet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import pexpect 3 | import os, sys, time 4 | 5 | ip = "127.0.0.1" 6 | port = "10000" 7 | username = "nikitapekin@gmail.com" 8 | password = "12345" 9 | 10 | try: 11 | os.remove('../maildir/.lock') 12 | except OSError: 13 | pass 14 | 15 | child = pexpect.spawn('telnet '+ ip + ' ' + port) 16 | 17 | child.expect('.\n') 18 | child.logfile = sys.stdout.buffer 19 | time.sleep(1) 20 | child.sendline('1 login ' + username + ' ' + password) 21 | child.expect('1 OK logged in successfully as nikitapekin@gmail.com') 22 | child.sendline('2 select INBOX') 23 | child.expect('successful') 24 | 25 | #child.sendline('3 fetch 1:2 RFC822.SIZE') 26 | #child.expect('completed') 27 | #child.sendline('3 fetch 1:2 RFC822.HEADER') 28 | #child.expect('completed') 29 | #child.sendline('3 fetch 1:2 (RFC822.SIZE RFC822.HEADER)') 30 | #child.expect('completed') 31 | 32 | #child.sendline('3 fetch 1:3 ENVELOPE') 33 | #child.expect('completed') 34 | 35 | #child.sendline('a3 fetch 1,2,3 UID') 36 | #child.expect('completed') 37 | 38 | #child.sendline('3 fetch 1:3 INTERNALDATE') 39 | #child.expect('completed') 40 | 41 | child.sendline('3 fetch 1 BODY.PEEK[]') 42 | child.expect('completed') 43 | 44 | child.sendline('3 fetch 1:3 (FLAGS UID)') 45 | child.expect('completed') 46 | 47 | child.sendline('4 UID fetch 1:* (FLAGS)') 48 | child.expect('completed') 49 | 50 | #child.sendline('3 fetch 1:2 (FLAGS BODY[HEADER.FIELDS (DATE FROM)])') 51 | #child.expect('unimplemented') 52 | -------------------------------------------------------------------------------- /telnet/telnet.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env sh 2 | ./telnet.py 3 | mv ../maildir/cur/1414871673 ../maildir/new/1414871673 4 | -------------------------------------------------------------------------------- /users.json.example: -------------------------------------------------------------------------------- 1 | [{"email":{"local_part":"nikitapekin","domain_part":"gmail.com"},"auth_data":{"salt":[100,66,49,66,86,74,55,113,99,89,97,113,52,80,122,108],"out":[233,135,75,177,83,47,121,39,199,12,145,208,10,57,67,76,115,42,58,22,190,126,131,183,222,71,186,38,114,129,190,118]},"maildir":"~/.maildir"},{"email":{"local_part":"will","domain_part":"xqz.ca"},"auth_data":{"salt":[110,100,54,51,110,113,81,98,97,80,57,99,66,111,109,66],"out":[200,87,185,81,141,243,250,160,29,113,219,114,98,11,43,98,32,31,85,54,49,107,113,47,243,232,174,103,125,124,198,219]},"maildir":"./maildir"}] --------------------------------------------------------------------------------