| 1 |
/* |
| 2 |
* Copyright (C) 2009-2017 TSUBAKIMOTO Hiroya <z0rac@users.sourceforge.jp> |
| 3 |
* |
| 4 |
* This software comes with ABSOLUTELY NO WARRANTY; for details of |
| 5 |
* the license terms, see the LICENSE.txt file included with the program. |
| 6 |
*/ |
| 7 |
#include "mailbox.h" |
| 8 |
#include "win32.h" |
| 9 |
#include <cassert> |
| 10 |
#include <cstdlib> |
| 11 |
#include <cstring> |
| 12 |
|
| 13 |
#if _DEBUG >= 2 |
| 14 |
#include <iostream> |
| 15 |
#define DBG(s) s |
| 16 |
#define LOG(s) (cout << s) |
| 17 |
#else |
| 18 |
#define DBG(s) |
| 19 |
#define LOG(s) |
| 20 |
#endif |
| 21 |
|
| 22 |
/** imap4 - imap4 protocol backend |
| 23 |
* This class is a mailbox::backend for IMAP4 protocol. |
| 24 |
*/ |
| 25 |
class imap4 : public mailbox::backend { |
| 26 |
unsigned _seq; // sequencial number for the tag. |
| 27 |
|
| 28 |
// parser - imap4 response parser. |
| 29 |
struct parser : public tokenizer { |
| 30 |
parser() {} |
| 31 |
parser(const string& s) : tokenizer(s) {} |
| 32 |
string token(bool open = false); |
| 33 |
}; |
| 34 |
|
| 35 |
// response - imap4 response type. |
| 36 |
struct response { string tag, type, data; }; |
| 37 |
|
| 38 |
static string _utf7m(const string& s); |
| 39 |
string _tag(); |
| 40 |
static string _arg(const string& arg); |
| 41 |
string _command(const char* cmd, const char* res = NULL); |
| 42 |
string _command(const string& cmd, const char* res = NULL) |
| 43 |
{ return _command(cmd.c_str(), res); } |
| 44 |
response _response(); |
| 45 |
string _read(); |
| 46 |
unsigned _seqinit() const { return unsigned(ptrdiff_t(this)) + unsigned(time(NULL)); } |
| 47 |
#ifdef _DEBUG |
| 48 |
using backend::read; |
| 49 |
string read() |
| 50 |
{ |
| 51 |
string line = backend::read(); |
| 52 |
LOG("R: " << line << endl); |
| 53 |
return line; |
| 54 |
} |
| 55 |
#endif |
| 56 |
public: |
| 57 |
imap4() : _seq(_seqinit()) {} |
| 58 |
void login(const uri& uri, const string& passwd); |
| 59 |
void logout(); |
| 60 |
size_t fetch(mailbox& mbox, const uri& uri); |
| 61 |
}; |
| 62 |
|
| 63 |
void |
| 64 |
imap4::login(const uri& uri, const string& passwd) |
| 65 |
{ |
| 66 |
static const char notimap[] = "server not IMAP4 compliant"; |
| 67 |
response resp = _response(); |
| 68 |
bool preauth = resp.type == "PREAUTH"; |
| 69 |
if (resp.tag != "*" || (!preauth && resp.type != "OK")) { |
| 70 |
throw mailbox::error(notimap); |
| 71 |
} |
| 72 |
bool imap = false; |
| 73 |
bool stls = false; |
| 74 |
static const char CAPABILITY[] = "CAPABILITY"; |
| 75 |
static const char STARTTLS[] = "STARTTLS"; |
| 76 |
string cap = _command(CAPABILITY, CAPABILITY); |
| 77 |
for (parser caps(cap); caps;) { |
| 78 |
string s = caps.token(); |
| 79 |
if (s == "IMAP4" || s == "IMAP4REV1") imap = true; |
| 80 |
else if (s == STARTTLS) stls = true; |
| 81 |
} |
| 82 |
if (!imap) throw mailbox::error(notimap); |
| 83 |
if (!preauth) { |
| 84 |
if (stls && !tls()) { |
| 85 |
_command(STARTTLS); |
| 86 |
starttls(uri[uri::host]); |
| 87 |
cap = _command(CAPABILITY, CAPABILITY); |
| 88 |
} |
| 89 |
for (parser caps(cap); caps;) { |
| 90 |
if (caps.token() == "LOGINDISABLED") { |
| 91 |
throw mailbox::error("login disabled"); |
| 92 |
} |
| 93 |
} |
| 94 |
_command("LOGIN" + _arg(uri[uri::user]) + _arg(passwd)); |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
void |
| 99 |
imap4::logout() |
| 100 |
{ |
| 101 |
_command("LOGOUT"); |
| 102 |
} |
| 103 |
|
| 104 |
size_t |
| 105 |
imap4::fetch(mailbox& mbox, const uri& uri) |
| 106 |
{ |
| 107 |
const string& path = uri[uri::path]; |
| 108 |
_command("EXAMINE" + _arg(!path.empty() ? _utf7m(path) : "INBOX")); |
| 109 |
list<mail> mails; |
| 110 |
list<mail> recents; |
| 111 |
for (parser ids(_command("UID SEARCH UNSEEN", "SEARCH")); ids;) { |
| 112 |
string uid = ids.token(); |
| 113 |
const mail* p = mbox.find(uid); |
| 114 |
if (p) { |
| 115 |
mails.push_back(*p); |
| 116 |
continue; |
| 117 |
} |
| 118 |
LOG("Fetch mail: " << uid << endl); |
| 119 |
parser parse(_command("UID FETCH " + uid + |
| 120 |
" BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]", |
| 121 |
"FETCH")); |
| 122 |
parse.token(); // drop sequence# |
| 123 |
if (parse.peek() != '(') throw mailbox::error(parse.data()); |
| 124 |
for (parse = parse.token(true); parse;) { |
| 125 |
string item = parse.token(); |
| 126 |
string value = parse.token(); |
| 127 |
if (item.size() > 20 && item.substr(0, 20) == "BODY[HEADER.FIELDS (") { |
| 128 |
mail m(uid); |
| 129 |
m.header(value); |
| 130 |
recents.push_back(m); |
| 131 |
break; |
| 132 |
} |
| 133 |
} |
| 134 |
} |
| 135 |
size_t count = recents.size(); |
| 136 |
mails.splice(mails.end(), recents); |
| 137 |
mbox.mails(mails); |
| 138 |
return count; |
| 139 |
} |
| 140 |
|
| 141 |
string |
| 142 |
imap4::_utf7m(const string& s) |
| 143 |
{ |
| 144 |
string::size_type i = 0; |
| 145 |
while (i < s.size() && s[i] >= ' ' && s[i] <= '~' && s[i] != '&') ++i; |
| 146 |
if (i == s.size()) return s; |
| 147 |
|
| 148 |
// encode path by modified UTF-7. |
| 149 |
string result = s.substr(0, i); |
| 150 |
win32::wstr ws = s.c_str() + i; |
| 151 |
for (LPCWSTR p = ws; *p;) { |
| 152 |
result += '&'; |
| 153 |
if (*p != '&') { |
| 154 |
static const char b64[] = |
| 155 |
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,"; |
| 156 |
unsigned wc = 0; |
| 157 |
int n = 0; |
| 158 |
while (*p && (*p < ' ' || *p > '~')) { |
| 159 |
wc = (wc << 16) + *p++, n += 16; |
| 160 |
for (; n >= 6; n -= 6) result += b64[(wc >> (n - 6)) & 63]; |
| 161 |
} |
| 162 |
if (n) result += b64[(wc << (6 - n)) & 63]; |
| 163 |
} else ++p; |
| 164 |
result += '-'; |
| 165 |
for (; *p && *p >= ' ' && *p <= '~' && *p != '&'; ++p) { |
| 166 |
result += static_cast<string::value_type>(*p); |
| 167 |
} |
| 168 |
} |
| 169 |
return result; |
| 170 |
} |
| 171 |
|
| 172 |
string |
| 173 |
imap4::_tag() |
| 174 |
{ |
| 175 |
unsigned n = _seq++; |
| 176 |
char s[4]; |
| 177 |
s[3] = '0' + n % 10, n /= 10; |
| 178 |
for (int i = 3; i--;) s[i] = 'A' + (n & 15), n >>= 4; |
| 179 |
return string(s, 4); |
| 180 |
} |
| 181 |
|
| 182 |
string |
| 183 |
imap4::_arg(const string& arg) |
| 184 |
{ |
| 185 |
if (!arg.empty()) { |
| 186 |
string::const_iterator p = arg.begin(); |
| 187 |
while (p != arg.end() && |
| 188 |
*p > 32 && *p < 127 && !strchr("(){%*\"\\", *p)) ++p; |
| 189 |
if (p == arg.end()) return ' ' + arg; |
| 190 |
} |
| 191 |
string esc(" \""); |
| 192 |
for (string::size_type i = 0;;) { |
| 193 |
string::size_type n = arg.find_first_of("\"\\", i); |
| 194 |
esc.append(arg, i, n - i); |
| 195 |
if (n == string::npos) break; |
| 196 |
char qst[] = { '\\', arg[n] }; |
| 197 |
esc.append(qst, 2); |
| 198 |
i = n + 1; |
| 199 |
} |
| 200 |
return esc + '"'; |
| 201 |
} |
| 202 |
|
| 203 |
string |
| 204 |
imap4::_command(const char* cmd, const char* res) |
| 205 |
{ |
| 206 |
const string tag = _tag(); |
| 207 |
// send a command message to the server. |
| 208 |
write(tag + ' ' + cmd); |
| 209 |
LOG("S: " << tag << " " << cmd << endl); |
| 210 |
|
| 211 |
response resp; |
| 212 |
string untagged; |
| 213 |
bool bye = false; |
| 214 |
for (;;) { |
| 215 |
resp = _response(); |
| 216 |
if (res && resp.type == "OK") { |
| 217 |
parser parse(resp.data); |
| 218 |
if (parse.peek() == '[') { |
| 219 |
parse = parse.token(true); |
| 220 |
if (parse.token() == res) untagged = parse.remain(); |
| 221 |
} |
| 222 |
} |
| 223 |
if (resp.tag != "*") break; |
| 224 |
if (resp.type == "BYE") bye = true; |
| 225 |
if (res && resp.type == res) untagged = resp.data; |
| 226 |
} |
| 227 |
if (resp.tag != tag) { |
| 228 |
throw mailbox::error("unexpected tagged response"); |
| 229 |
} |
| 230 |
if (bye && _stricmp(cmd, "LOGOUT") != 0) throw mailbox::error("bye"); |
| 231 |
if (resp.type != "OK") { |
| 232 |
throw mailbox::error(resp.type + ' ' + resp.data); |
| 233 |
} |
| 234 |
return untagged; |
| 235 |
} |
| 236 |
|
| 237 |
imap4::response |
| 238 |
imap4::_response() |
| 239 |
{ |
| 240 |
parser parse(_read()); |
| 241 |
response resp; |
| 242 |
resp.tag = parse.token(); |
| 243 |
if (resp.tag == "+") { // continuation |
| 244 |
resp.data = parse.remain(); |
| 245 |
return resp; |
| 246 |
} |
| 247 |
resp.type = parse.token(); |
| 248 |
if (resp.tag.empty() || resp.type.empty()) { |
| 249 |
throw mailbox::error("unexpected response: " + parse.data()); |
| 250 |
} |
| 251 |
if (parse && parser::digit(resp.type)) { |
| 252 |
resp.data = resp.type, resp.type = parse.token(); |
| 253 |
} |
| 254 |
if (parse) { |
| 255 |
if (!resp.data.empty()) resp.data += ' '; |
| 256 |
resp.data += parse.remain(); |
| 257 |
} |
| 258 |
return resp; |
| 259 |
} |
| 260 |
|
| 261 |
string |
| 262 |
imap4::_read() |
| 263 |
{ |
| 264 |
string line = read(); |
| 265 |
if (!line.empty() && line[0] != '+') { |
| 266 |
while (line[line.size() - 1] == '}') { |
| 267 |
string::size_type i = line.find_last_of('{'); |
| 268 |
if (i == string::npos) break; |
| 269 |
const char* p = line.c_str(); |
| 270 |
char* end; |
| 271 |
size_t size = strtoul(p + i + 1, &end, 10); |
| 272 |
if (string::size_type(end - p) != line.size() - 1) break; |
| 273 |
if (size) { // read literal data. |
| 274 |
string literal = read(size); |
| 275 |
LOG(literal); |
| 276 |
line += literal; |
| 277 |
} |
| 278 |
line += read(); // read a following line. |
| 279 |
} |
| 280 |
} |
| 281 |
return line; |
| 282 |
} |
| 283 |
|
| 284 |
/* |
| 285 |
* Functions of the class imap4::parser |
| 286 |
*/ |
| 287 |
string |
| 288 |
imap4::parser::token(bool open) |
| 289 |
{ |
| 290 |
string result; |
| 291 |
if (*this) { |
| 292 |
char delim[] = " [(\"{"; |
| 293 |
string::size_type i = findf(delim); |
| 294 |
result = uppercase(i); |
| 295 |
if (i == string::npos) { |
| 296 |
_next = _s.size(); |
| 297 |
} else if (_s[i] == ' ') { |
| 298 |
_next = i + 1; |
| 299 |
} else if (_s[i] == '[' || result.empty()) { |
| 300 |
result += _s[i]; |
| 301 |
for (string st(1, _s[i++]); !st.empty();) { |
| 302 |
_next = i; |
| 303 |
if (_next >= _s.size()) { |
| 304 |
throw mailbox::error("invalid token: " + _s); |
| 305 |
} |
| 306 |
string::size_type sp = st.size() - 1; |
| 307 |
switch (st[sp]) { |
| 308 |
case '"': |
| 309 |
i = findq("\"", i); |
| 310 |
if (i != string::npos) { |
| 311 |
result.append(_s, _next, ++i - _next); |
| 312 |
st.erase(sp); |
| 313 |
} |
| 314 |
break; |
| 315 |
case '{': |
| 316 |
{ |
| 317 |
const char* p = _s.c_str(); |
| 318 |
char* end; |
| 319 |
i = strtoul(p + i, &end, 10); |
| 320 |
i += string::size_type(end - p) + 1; |
| 321 |
if (*end == '}' && i <= _s.size()) { |
| 322 |
result.append(_s, _next, i - _next); |
| 323 |
st.erase(sp); |
| 324 |
} else { |
| 325 |
i = string::npos; |
| 326 |
} |
| 327 |
} |
| 328 |
break; |
| 329 |
default: |
| 330 |
delim[0] = st[sp] == '[' ? ']' : ')'; |
| 331 |
i = findf(delim, i); |
| 332 |
if (i != string::npos) { |
| 333 |
if (_s[i] == delim[0]) st.erase(sp); |
| 334 |
else st.push_back(_s[i]); |
| 335 |
result += uppercase(++i); |
| 336 |
} |
| 337 |
break; |
| 338 |
} |
| 339 |
} |
| 340 |
_next = i < _s.size() && _s[i] == ' ' ? i + 1 : i; |
| 341 |
if (result.size() > 1) { |
| 342 |
switch (result[0]) { |
| 343 |
default: |
| 344 |
if (!open || (result[0] != '(' && result[0] != '[')) break; |
| 345 |
case '"': |
| 346 |
result.assign(result, 1, result.size() - 2); |
| 347 |
break; |
| 348 |
case '{': |
| 349 |
assert(result.find_first_of('}') != string::npos); |
| 350 |
result.erase(0, result.find_first_of('}') + 1); |
| 351 |
break; |
| 352 |
} |
| 353 |
} |
| 354 |
} |
| 355 |
} |
| 356 |
return result; |
| 357 |
} |
| 358 |
|
| 359 |
mailbox::backend* backendIMAP4() { return new imap4; } |