Skip to content

Commit

Permalink
Decode/encode mailbox names. Close #69.
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Virkus committed Jul 4, 2020
1 parent e1c72f7 commit 001771d
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 19 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.0.29
- Add `discconect()` method to high level `MailClient` API
- Encode and decode mailbox names using Modified UTF7 encoding
- Add [IMAP support for UTF-8](https://tools.ietf.org/html/rfc6855)

## 0.0.28
- High level `MailClient` API supports IMAP IDLE, POP and SMTP.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ The following IMAP extensions are supported:
*[CONDSTORE](https://tools.ietf.org/html/rfc7162)
*[QRESYNC](https://tools.ietf.org/html/rfc7162)
*[ENABLE](https://tools.ietf.org/html/rfc5161)

*[IMAP Support for UTF-8](https://tools.ietf.org/html/rfc6855)

### Supported encodings
Character encodings:
Expand Down
3 changes: 3 additions & 0 deletions lib/codecs/mail_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ abstract class MailCodec {
static const String _encodingEndSequence = '?=';
static final RegExp _encodingExpression = RegExp(
r'\=\?.+?\?.+?\?.+?\?\='); // the question marks after plus make this regular expression non-greedy
static const Encoding encodingUtf8 = utf8;
static const Encoding encodingLatin1 = latin1;
static const Encoding encodingAscii = ascii;
static final Map<String, Encoding> _codecsByName = <String, Encoding>{
'utf-8': utf8,
'utf8': utf8,
Expand Down
257 changes: 257 additions & 0 deletions lib/codecs/modified_utf7_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import 'dart:convert';

/// Provides Modified UTF7 encoder and decoder.
/// Compare https://tools.ietf.org/html/rfc3501#section-5.1.3 and https://tools.ietf.org/html/rfc2152 for details.
/// Inspired by https://github.com/jstedfast/MailKit/blob/master/MailKit/Net/Imap/ImapEncoding.cs
class ModifiedUtf7Codec {
const ModifiedUtf7Codec();

static const String _utf7Alphabet =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,';

static const List<int> _utf7Rank = [
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
62,
63,
255,
255,
255,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
255,
255,
255,
255,
255,
255,
255,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
255,
255,
255,
255,
255,
255,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
255,
255,
255,
255,
255,
];

void _utf7ShiftOut(StringBuffer output, int u, int bits) {
if (bits > 0) {
final x = (u << (6 - bits)) & 0x3f;
output.write(_utf7Alphabet[x]);
}

output.write('-');
}

/// Encodes the specified text in Modified UTF7 format.
/// [text] specifies the text to be encoded.
String encodeText(String text) {
final encoded = StringBuffer();
var shifted = false;
var bits = 0, u = 0;

for (var index = 0; index < text.length; index++) {
var character = text[index];
var codeUnit = character.codeUnitAt(0);
if (codeUnit >= 0x20 && codeUnit < 0x7f) {
// characters with octet values 0x20-0x25 and 0x27-0x7e
// represent themselves while 0x26 ("&") is represented
// by the two-octet sequence "&-"

if (shifted) {
_utf7ShiftOut(encoded, u, bits);
shifted = false;
bits = 0;
}

if (codeUnit == 0x26) {
encoded.write('&-');
} else {
encoded.write(character);
}
} else {
// base64 encode
if (!shifted) {
encoded.write('&');
shifted = true;
}

u = (u << 16) | (codeUnit & 0xffff);
bits += 16;

while (bits >= 6) {
final x = (u >> (bits - 6)) & 0x3f;
encoded.write(_utf7Alphabet[x]);
bits -= 6;
}
}
}

if (shifted) {
_utf7ShiftOut(encoded, u, bits);
}

return encoded.toString();
}

/// Decodes the specified [text]
///
/// [codec] the optional character encoding (charset, defaults to utf-8)
String decodeText(String text, [Encoding codec = utf8]) {
var decoded = StringBuffer();
var shifted = false;
var bits = 0, v = 0;
var index = 0;
String c;

while (index < text.length) {
c = text[index++];

if (shifted) {
final codeUnit = c.codeUnitAt(0);
if (c == '-') {
// shifted back out of modified UTF-7
shifted = false;
bits = v = 0;
} else if (codeUnit > 127) {
// invalid UTF-7
return text;
} else {
final rank = _utf7Rank[codeUnit];

if (rank == 0xff) {
// invalid UTF-7
return text;
}

v = (v << 6) | rank;
bits += 6;

if (bits >= 16) {
var u = ((v >> (bits - 16)) & 0xffff);
decoded.write(String.fromCharCode(u));
bits -= 16;
}
}
} else if (c == '&' && index < text.length) {
if (text[index] == '-') {
decoded.write('&');
index++;
} else {
// shifted into modified UTF-7
shifted = true;
}
} else {
decoded.write(c);
}
}

return decoded.toString();
}
}
44 changes: 36 additions & 8 deletions lib/imap/imap_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ class ImapServerInfo {
String capabilitiesText;
List<Capability> capabilities;
List<Capability> enabledCapabilities = <Capability>[];

/// Checks if the capability with the specified [capabilityName] is supported.
bool supports(String capabilityName) {
return (capabilities?.firstWhere((c) => c.name == capabilityName,
orElse: () => null) !=
null);
}

/// Checks if the capability with the specified [capabilityName] has been enabled.
bool isEnabled(String capabilityName) {
return (enabledCapabilities?.firstWhere((c) => c.name == capabilityName,
orElse: () => null) !=
null);
}
}

enum StoreAction { add, remove, replace }
Expand Down Expand Up @@ -591,11 +605,17 @@ class ImapClient {
path, (recursive ? '*' : '%')); // list all folders in that path
}

String _encodeMailboxPath(String path) {
return (serverInfo.isEnabled('UTF8=ACCEPT')) ? path : Mailbox.encode(path);
}

/// lists all mailboxes in the path [referenceName] that match the given [mailboxName] that can contain wildcards.
///
/// The LIST command will set the [serverInfo.pathSeparator] as a side-effect
Future<Response<List<Mailbox>>> listMailboxesByReferenceAndName(
String referenceName, String mailboxName) {
referenceName = _encodeMailboxPath(referenceName);
mailboxName = _encodeMailboxPath(mailboxName);
var cmd = Command('LIST $referenceName $mailboxName');
var parser = ListParser(serverInfo);
return sendCommand<List<Mailbox>>(cmd, parser);
Expand All @@ -608,7 +628,7 @@ class ImapClient {
/// The LIST command will set the [serverInfo.pathSeparator] as a side-effect
Future<Response<List<Mailbox>>> listSubscribedMailboxes(
{String path = '""', bool recursive = false}) {
//Command cmd = Command("LIST \"INBOX/\" %");
path = _encodeMailboxPath(path);
var cmd = Command('LSUB $path ' +
(recursive ? '*' : '%')); // list all folders in that path
var parser = ListParser(serverInfo, isLsubParser: true);
Expand Down Expand Up @@ -687,7 +707,8 @@ class ImapClient {
/// implementation for both SELECT as well as EXAMINE
Future<Response<Mailbox>> _selectOrExamine(String command, Mailbox box,
{bool enableCondStore = false, QResyncParameters qresync}) {
var buffer = StringBuffer()..write(command)..write(' ')..write(box.path);
var path = _encodeMailboxPath(box.path);
var buffer = StringBuffer()..write(command)..write(' ')..write(path);
if (enableCondStore || qresync != null) {
buffer.write(' (');
if (enableCondStore) {
Expand Down Expand Up @@ -1036,6 +1057,7 @@ class ImapClient {
///
/// Spefify the name with [path]
Future<Response<Mailbox>> createMailbox(String path) async {
path = _encodeMailboxPath(path);
var cmd = Command('CREATE $path');
var response = await sendCommand<Mailbox>(cmd, null);
if (response.isOkStatus) {
Expand All @@ -1054,16 +1076,18 @@ class ImapClient {
///
/// [box] the mailbox to be deleted
Future<Response<Mailbox>> deleteMailbox(Mailbox box) {
var cmd = Command('DELETE ${box.path}');
return sendCommand<Mailbox>(cmd, null);
return _sendMailboxCommand('DELETE', box);
}

/// Renames the specified mailbox
///
/// [box] the mailbox that should be renamed
/// [newName] the desired future name of the mailbox
Future<Response<Mailbox>> renameMailbox(Mailbox box, String newName) async {
var cmd = Command('RENAME ${box.path} $newName');
var path = _encodeMailboxPath(box.path);
newName = _encodeMailboxPath(newName);

var cmd = Command('RENAME ${path} $newName');
var response = await sendCommand<Mailbox>(cmd, null);
if (response.isOkStatus) {
if (box.name == 'INBOX') {
Expand All @@ -1085,15 +1109,19 @@ class ImapClient {
/// The mailbox is listed in future LSUB commands, compare [listSubscribedMailboxes].
/// [box] the mailbox that is subscribed
Future<Response<Mailbox>> subscribeMailbox(Mailbox box) {
var cmd = Command('SUBSCRIBE ${box.path}');
return sendCommand<Mailbox>(cmd, null);
return _sendMailboxCommand('SUBSCRIBE', box);
}

/// Unsubscribes the specified mailbox.
///
/// [box] the mailbox that is unsubscribed
Future<Response<Mailbox>> unsubscribeMailbox(Mailbox box) {
var cmd = Command('UNSUBSCRIBE ${box.path}');
return _sendMailboxCommand('UNSUBSCRIBE', box);
}

Future<Response<Mailbox>> _sendMailboxCommand(String command, Mailbox box) {
var path = _encodeMailboxPath(box.path);
var cmd = Command('$command $path');
return sendCommand<Mailbox>(cmd, null);
}

Expand Down
Loading

0 comments on commit 001771d

Please sign in to comment.