This commit is contained in:
2025-02-16 07:49:06 +03:00
parent 01db4540b1
commit 7f4fc42a7a
32 changed files with 8223 additions and 297 deletions

View File

@@ -1,9 +0,0 @@
module imap
import net
// handle_capability processes the CAPABILITY command
pub fn handle_capability(mut conn net.TcpConn, tag string) ! {
conn.write('* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS LOGIN\r\n'.bytes())!
conn.write('${tag} OK Completed\r\n'.bytes())!
}

View File

@@ -1,49 +0,0 @@
module imap
import net
import strconv
// handle_fetch processes the FETCH command
pub fn (mut self Session) handle_fetch( tag string, parts []string) ! {
mut mailbox:=self.mailbox()!
// For simplicity, we support commands like: A001 FETCH 1:* BODY[TEXT]
if parts.len < 4 {
conn.write('${tag} BAD FETCH requires a message sequence and data item\r\n'.bytes())!
return
}
sequence := parts[2]
selected_mailbox := server.mailboxes[mailbox_name]
// If the sequence is 1:*, iterate over all messages.
if sequence == '1:*' {
for i, msg in selected_mailbox.messages {
flags_str := if msg.flags.len > 0 {
'(' + msg.flags.join(' ') + ')'
} else {
'()'
}
// In a full implementation, more attributes would be returned.
conn.write('* ${i+1} FETCH (FLAGS ${flags_str} BODY[TEXT] "${msg.body}")\r\n'.bytes())!
}
conn.write('${tag} OK FETCH completed\r\n'.bytes())!
return true
} else {
// Otherwise, parse a single message number
index := strconv.atoi(parts[2]) or {
conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
return
} - 1
if index < 0 || index >= server.mailboxes[mailbox_name].messages.len {
conn.write('${tag} BAD Invalid message sequence\r\n'.bytes())!
} else {
msg := selected_mailbox.messages[index]
flags_str := if msg.flags.len > 0 {
'(' + msg.flags.join(' ') + ')'
} else {
'()'
}
conn.write('* ${index+1} FETCH (FLAGS ${flags_str} BODY[TEXT] "${msg.body}")\r\n'.bytes())!
conn.write('${tag} OK FETCH completed\r\n'.bytes())!
return true
}
}
}

View File

@@ -1,13 +0,0 @@
module imap
import net
// handle_login processes the LOGIN command
pub fn (mut self Session) handle_login(mut conn net.TcpConn, tag string, parts []string, server &IMAPServer) ! {
if parts.len < 4 {
conn.write('${tag} BAD LOGIN requires username and password\r\n'.bytes())!
return
}
// For demo purposes, accept any username/password
conn.write('${tag} OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] User logged in\r\n'.bytes())!
}

View File

@@ -1,9 +0,0 @@
module imap
import net
// handle_logout processes the LOGOUT command
pub fn (mut self Session) handle_logout(mut conn net.TcpConn, tag string) ! {
conn.write('* BYE IMAP4rev1 Server logging out\r\n'.bytes())!
conn.write('${tag} OK LOGOUT completed\r\n'.bytes())!
}

View File

@@ -1,27 +0,0 @@
module imap
import net
// handle_select processes the SELECT command
pub fn (mut self Session) handle_select(mut conn net.TcpConn, tag string, parts []string, mut server IMAPServer) !string {
if parts.len < 3 {
conn.write('${tag} BAD SELECT requires a mailbox name\r\n'.bytes())!
return error('SELECT requires a mailbox name')
}
// Remove any surrounding quotes from mailbox name
mailbox_name := parts[2].trim('"')
// Look for the mailbox. If not found, create it.
if mailbox_name !in server.mailboxes {
server.mailboxes[mailbox_name] = &Mailbox{
name: mailbox_name
messages: []Message{}
}
}
// Respond with a basic status.
messages_count := server.mailboxes[mailbox_name].messages.len
conn.write('* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n'.bytes())!
conn.write('* ${messages_count} EXISTS\r\n'.bytes())!
conn.write('* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted\r\n'.bytes())!
conn.write('${tag} OK [READ-WRITE] SELECT completed\r\n'.bytes())!
return mailbox_name
}

View File

@@ -1,61 +0,0 @@
module imap
import net
import strconv
// handle_store processes the STORE command
pub fn (mut self Session) handle_store(mut conn net.TcpConn, tag string, parts []string, mut server IMAPServer, mailbox_name string) !bool {
if mailbox_name !in server.mailboxes {
conn.write('${tag} BAD No mailbox selected\r\n'.bytes())!
return
}
// Expecting a format like: A003 STORE 1 +FLAGS (\Seen)
if parts.len < 5 {
conn.write('${tag} BAD STORE requires a message sequence, an operation, and flags\r\n'.bytes())!
return
}
// For simplicity, only support a single message number.
index := strconv.atoi(parts[2]) or {
conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
return
} - 1
if index < 0 || index >= server.mailboxes[mailbox_name].messages.len {
conn.write('${tag} BAD Invalid message sequence\r\n'.bytes())!
return
}
op := parts[3] // e.g. "+FLAGS", "-FLAGS", or "FLAGS"
// The flags are provided in the next token, e.g.: (\Seen)
flags_str := parts[4]
// Remove any surrounding parentheses.
flags_clean := flags_str.trim('()')
flags_arr := flags_clean.split(' ').filter(it != '')
mut msg := server.mailboxes[mailbox_name].messages[index]
match op {
'+FLAGS' {
// Add each flag if it isn't already present.
for flag in flags_arr {
if flag !in msg.flags {
msg.flags << flag
}
}
}
'-FLAGS' {
// Remove any flags that match.
for flag in flags_arr {
msg.flags = msg.flags.filter(it != flag)
}
}
'FLAGS' {
// Replace current flags.
msg.flags = flags_arr
}
else {
conn.write('${tag} BAD Unknown STORE operation\r\n'.bytes())!
return
}
}
// Save the updated message back.
server.mailboxes[mailbox_name].messages[index] = msg
conn.write('${tag} OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)] Store completed\r\n'.bytes())!
return true
}

View File

@@ -1,47 +0,0 @@
module imap
import net
import io
// Represents an email message.
pub struct Message {
pub mut:
id int
subject string
body string
flags []string // e.g.: ["\\Seen", "\\Flagged"]
}
// Represents a mailbox holding messages.
pub struct Mailbox {
pub mut:
name string
messages []Message
}
// Our in-memory server holds a map of mailbox names to pointers to Mailbox.
pub struct IMAPServer {
pub mut:
mailboxes map[string]&Mailbox
}
pub struct Session {
pub mut:
server &IMAPServer
mailbox string //the name of the mailbox
conn net.TcpConn
reader &io.BufferedReader
}
pub fn (mut self Session) mailbox_new(name string) !&Mailbox{
self.mailboxes[name] = &Mailbox{name:name}
return self.mailboxes[name]
}
pub fn (mut self Session) mailbox() !&Mailbox{
if !(mailbox_name in server.mailboxes) {
return error ("mailbox ${self.mailbox} does not exist")
}
return self.mailboxes[self.mailbox] or { panic(err) }
}

View File

@@ -1,60 +0,0 @@
The Internet Message Access Protocol Version 4 Revision 1 (IMAP4rev1) enables clients to access and manage email messages on a server, treating remote mailboxes as if they were local folders. This protocol supports operations such as creating, deleting, and renaming mailboxes; checking for new messages; permanently removing messages; setting and clearing flags; parsing messages; searching; and selectively fetching message attributes and content. IMAP4rev1 is designed for single-server access and does not include functionality for sending emails, which is typically handled by protocols like SMTP.
## Introduction
IMAP4rev1 allows clients to interact with email messages on a server, providing functionalities similar to local email management. It enables seamless synchronization between clients and servers, facilitating both online and offline email access.
## Protocol Overview
IMAP4rev1 operates over a reliable data stream, typically TCP. Communication involves clients sending commands to the server and receiving responses. Servers can also send untagged responses to inform clients of real-time updates.
## Commands and Responses
- **Commands**: Issued by the client to perform actions like selecting a mailbox or fetching messages.
- **Responses**: Returned by the server to indicate the status of a command or to provide requested data.
Commands are categorized as follows:
- **Authentication Commands**: Manage client authentication.
- **Mailbox Commands**: Handle mailbox selection and status.
- **Message Commands**: Operate on messages within a mailbox.
## Mailbox Operations
Clients can perform various operations on mailboxes, including:
- **Create**: Establish a new mailbox.
- **Delete**: Remove an existing mailbox.
- **Rename**: Change the name of a mailbox.
- **Subscribe/Unsubscribe**: Manage mailbox subscriptions.
- **List**: Retrieve a list of mailboxes.
- **Status**: Obtain the status of a mailbox, such as message count.
## Message Operations
Within mailboxes, clients can:
- **Fetch**: Retrieve specific message data.
- **Search**: Find messages matching certain criteria.
- **Store**: Alter message attributes, like flags.
- **Copy**: Duplicate messages to another mailbox.
- **Expunge**: Permanently remove messages marked for deletion.
## Client and Server Responsibilities
- **Client**: Initiates commands, processes server responses, and maintains synchronization with the server.
- **Server**: Executes client commands, sends appropriate responses, and manages the state of mailboxes and messages.
## Security Considerations
IMAP4rev1 does not inherently provide encryption. It's essential to implement security measures such as TLS to protect data transmitted between clients and servers.
## References
- **IMAP4rev1 Specification**: [RFC 3501](https://datatracker.ietf.org/doc/html/rfc3501)
- **Email Message Format**: [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822)
- **MIME Standards**: [RFC 2045](https://datatracker.ietf.org/doc/html/rfc2045)
- **Multiple Mailbox Support**: [RFC 2244](https://datatracker.ietf.org/doc/html/rfc2244)
- **Mail Transfer Protocol**: [RFC 2821](https://datatracker.ietf.org/doc/html/rfc2821)
For a comprehensive understanding and detailed technical specifications, refer to the full [RFC 3501 document](https://datatracker.ietf.org/doc/html/rfc3501).

View File

@@ -4,7 +4,7 @@ A simple IMAP server implementation in V that supports basic mailbox operations.
## Features
- In-memory IMAP server implementation
- IMAP server implementation with persistent storage via mailbox module
- Support for multiple mailboxes
- Basic IMAP commands: LOGIN, SELECT, FETCH, STORE, LOGOUT
- Message flags support (e.g. \Seen, \Flagged)
@@ -48,9 +48,9 @@ curl "imap://localhost/INBOX" -u "user:pass" --ssl-reqd
The server consists of three main components:
1. **Model** (`model.v`): Defines the core data structures
- `Message`: Represents an email message with ID, subject, body and flags
- `Mailbox`: Contains a collection of messages
- `IMAPServer`: Holds the mailboxes map
- Uses the `mailbox` module for message storage and retrieval
- Handles mailbox operations through a standardized interface
- Provides message and mailbox management functionality
2. **Server** (`server.v`): Handles the IMAP protocol implementation
- TCP connection handling
@@ -60,6 +60,7 @@ The server consists of three main components:
3. **Factory** (`factory.v`): Provides easy server initialization
- `start()` function to create and run the server
- Initializes example INBOX with sample messages
- Sets up mailbox storage backend
## Supported Commands
@@ -107,7 +108,6 @@ S: A007 OK LOGOUT completed
- The server runs on port 143, which typically requires root privileges. Make sure you have the necessary permissions.
- This is a basic implementation for demonstration purposes. For production use, consider adding:
- Proper authentication
- Persistent storage
- Full IMAP command support
- TLS encryption
- Message parsing and MIME support

View File

@@ -7,15 +7,15 @@ import freeflowuniverse.herolib.ui.console
// handle_authenticate processes the AUTHENTICATE command
pub fn (mut self Session) handle_authenticate(tag string, parts []string) ! {
if parts.len < 3 {
conn.write('${tag} BAD AUTHENTICATE requires an authentication mechanism\r\n'.bytes())!
self.conn.write('${tag} BAD AUTHENTICATE requires an authentication mechanism\r\n'.bytes())!
return
}
auth_type := parts[2].to_upper()
if auth_type == 'PLAIN' {
// Send continuation request for credentials
conn.write('+ \r\n'.bytes())!
self.conn.write('+ \r\n'.bytes())!
// Read base64 credentials
creds := reader.read_line() or {
creds := self.reader.read_line() or {
match err.msg() {
'closed' {
console.print_debug('Client disconnected during authentication')
@@ -33,11 +33,13 @@ pub fn (mut self Session) handle_authenticate(tag string, parts []string) ! {
}
if creds.len > 0 {
// For demo purposes, accept any credentials
conn.write('${tag} OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] Authentication successful\r\n'.bytes())!
// After successful auth, remove STARTTLS and LOGINDISABLED capabilities
self.capabilities = ['IMAP4rev2', 'AUTH=PLAIN']
self.conn.write('${tag} OK [CAPABILITY IMAP4rev2 AUTH=PLAIN] Authentication successful\r\n'.bytes())!
} else {
conn.write('${tag} NO Authentication failed\r\n'.bytes())!
self.conn.write('${tag} NO Authentication failed\r\n'.bytes())!
}
} else {
conn.write('${tag} NO [ALERT] Unsupported authentication mechanism\r\n'.bytes())!
self.conn.write('${tag} NO [ALERT] Unsupported authentication mechanism\r\n'.bytes())!
}
}

View File

@@ -0,0 +1,28 @@
module imap
import net
// handle_capability processes the CAPABILITY command
// See RFC 3501 Section 6.1.1
pub fn (mut self Session) handle_capability(tag string) ! {
mut capabilities := []string{}
// IMAP4rev2 is required and must be included
capabilities << 'IMAP4rev2'
// Required capabilities on cleartext ports
if !self.tls_active {
capabilities << 'STARTTLS'
capabilities << 'LOGINDISABLED'
}
// Required AUTH capability
capabilities << 'AUTH=PLAIN'
// Send capabilities in untagged response
// Note: IMAP4rev2 doesn't need to be first, but must be included
self.conn.write('* CAPABILITY ${capabilities.join(' ')}\r\n'.bytes())!
// Send tagged OK response
self.conn.write('${tag} OK CAPABILITY completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,29 @@
module imap
import net
// handle_close processes the CLOSE command
// See RFC 3501 Section 6.4.1
pub fn (mut self Session) handle_close(tag string) ! {
// If no mailbox is selected, return error
if self.mailbox == '' {
self.conn.write('${tag} NO No mailbox selected\r\n'.bytes())!
return
}
mut mbox := self.mailbox()!
// Remove all messages with \Deleted flag
mut new_messages := []Message{}
for msg in mbox.messages {
if '\\Deleted' !in msg.flags {
new_messages << msg
}
}
mbox.messages = new_messages
// Clear selected mailbox
self.mailbox = ''
self.conn.write('${tag} OK CLOSE completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,153 @@
module imap
import net
import strconv
// handle_fetch processes the FETCH command
// See RFC 3501 Section 6.4.5
pub fn (mut self Session) handle_fetch(tag string, parts []string) ! {
mut mailbox := self.mailbox()!
if parts.len < 4 {
self.conn.write('${tag} BAD FETCH requires a message sequence and data item\r\n'.bytes())!
return
}
sequence := parts[2]
// Join remaining parts to handle parenthesized items
data_items := parts[3..].join(' ').trim('()')
// Parse data items, handling quoted strings and parentheses
mut items_to_fetch := []string{}
mut current_item := ''
mut in_brackets := false
for c in data_items {
match c {
`[` {
in_brackets = true
current_item += c.ascii_str()
}
`]` {
in_brackets = false
current_item += c.ascii_str()
if current_item != '' {
items_to_fetch << current_item.trim_space()
current_item = ''
}
}
` ` {
if in_brackets {
current_item += c.ascii_str()
} else if current_item != '' {
items_to_fetch << current_item.trim_space()
current_item = ''
}
}
else {
current_item += c.ascii_str()
}
}
}
if current_item != '' {
items_to_fetch << current_item.trim_space()
}
// Convert to uppercase for matching
items_to_fetch = items_to_fetch.map(it.to_upper())
// Parse sequence range
mut start_idx := 0
mut end_idx := 0
if sequence == '1:*' {
start_idx = 0
end_idx = mailbox.messages.len - 1
} else if sequence.contains(':') {
range_parts := sequence.split(':')
if range_parts.len != 2 {
self.conn.write('${tag} BAD Invalid sequence range\r\n'.bytes())!
return
}
start_idx = strconv.atoi(range_parts[0]) or {
self.conn.write('${tag} BAD Invalid sequence range start\r\n'.bytes())!
return
} - 1
if range_parts[1] == '*' {
end_idx = mailbox.messages.len - 1
} else {
end_idx = strconv.atoi(range_parts[1]) or {
self.conn.write('${tag} BAD Invalid sequence range end\r\n'.bytes())!
return
} - 1
}
} else {
// Single message number
start_idx = strconv.atoi(sequence) or {
self.conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
return
} - 1
end_idx = start_idx
}
if start_idx < 0 || end_idx >= mailbox.messages.len || start_idx > end_idx {
self.conn.write('${tag} NO Invalid message range\r\n'.bytes())!
return
}
// Process messages in range
for i := start_idx; i <= end_idx; i++ {
msg := mailbox.messages[i]
mut response := []string{}
// Always include UID in FETCH responses
response << 'UID ${msg.uid}'
for item in items_to_fetch {
match item {
'FLAGS' {
flags_str := if msg.flags.len > 0 {
msg.flags.join(' ')
} else {
''
}
response << 'FLAGS (${flags_str})'
}
'INTERNALDATE' {
response << 'INTERNALDATE "${msg.internal_date.str()}"'
}
'RFC822.SIZE' {
response << 'RFC822.SIZE ${msg.body.len}'
}
'BODY[TEXT]' {
response << 'BODY[TEXT] {${msg.body.len}}\r\n${msg.body}'
}
'BODY[]', 'BODY.PEEK[]' {
// For BODY[], return the full message including headers
mut full_msg := 'From: <>\r\n'
full_msg += 'Subject: ${msg.subject}\r\n'
full_msg += 'Date: ${msg.internal_date.str()}\r\n'
full_msg += '\r\n' // Empty line between headers and body
full_msg += msg.body
response << 'BODY[] {${full_msg.len}}\r\n${full_msg}'
}
'BODY[HEADER]', 'BODY.PEEK[HEADER]' {
// Return just the headers
mut headers := 'From: <>\r\n'
headers += 'Subject: ${msg.subject}\r\n'
headers += 'Date: ${msg.internal_date.str()}\r\n'
headers += '\r\n' // Empty line after headers
response << 'BODY[HEADER] {${headers.len}}\r\n${headers}'
}
'ENVELOPE' {
// Basic envelope with just subject for now
response << 'ENVELOPE (NIL "${msg.subject}" NIL NIL NIL NIL NIL NIL NIL NIL)'
}
else {}
}
}
self.conn.write('* ${i+1} FETCH (${response.join(' ')})\r\n'.bytes())!
}
self.conn.write('${tag} OK FETCH completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,114 @@
module imap
import net
// handle_list processes the LIST command
// See RFC 3501 Section 6.3.9
pub fn (mut self Session) handle_list(tag string, parts []string) ! {
if parts.len < 4 {
self.conn.write('${tag} BAD LIST requires reference name and mailbox name\r\n'.bytes())!
return
}
reference := parts[2].trim('"')
pattern := parts[3].trim('"')
// For now, we only support empty reference and simple patterns
if reference != '' && reference != 'INBOX' {
// Just return OK with no results for unsupported references
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
return
}
// Handle special case of empty mailbox name
if pattern == '' {
// Return hierarchy delimiter and root name
self.conn.write('* LIST (\\Noselect) "/" ""\r\n'.bytes())!
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
return
}
// Handle % wildcard (single level)
if pattern == '%' {
// List top-level mailboxes
for name, mbox in self.server.mailboxes {
if !name.contains('/') { // Only top level
mut attrs := []string{}
if mbox.read_only {
attrs << '\\ReadOnly'
}
// Add child status attributes
mut has_children := false
for other_name, _ in self.server.mailboxes {
if other_name.starts_with(name + '/') {
has_children = true
break
}
}
if has_children {
attrs << '\\HasChildren'
} else {
attrs << '\\HasNoChildren'
}
attr_str := if attrs.len > 0 { '(${attrs.join(' ')})' } else { '()' }
self.conn.write('* LIST ${attr_str} "/" "${name}"\r\n'.bytes())!
}
}
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
return
}
// Handle * wildcard (multiple levels)
if pattern == '*' {
// List all mailboxes
for name, mbox in self.server.mailboxes {
mut attrs := []string{}
if mbox.read_only {
attrs << '\\ReadOnly'
}
// Add child status attributes
mut has_children := false
for other_name, _ in self.server.mailboxes {
if other_name.starts_with(name + '/') {
has_children = true
break
}
}
if has_children {
attrs << '\\HasChildren'
} else {
attrs << '\\HasNoChildren'
}
attr_str := if attrs.len > 0 { '(${attrs.join(' ')})' } else { '()' }
self.conn.write('* LIST ${attr_str} "/" "${name}"\r\n'.bytes())!
}
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
return
}
// Handle exact mailbox name
if pattern in self.server.mailboxes {
mbox := self.server.mailboxes[pattern]
mut attrs := []string{}
if mbox.read_only {
attrs << '\\ReadOnly'
}
// Add child status attributes
mut has_children := false
for other_name, _ in self.server.mailboxes {
if other_name.starts_with(pattern + '/') {
has_children = true
break
}
}
if has_children {
attrs << '\\HasChildren'
} else {
attrs << '\\HasNoChildren'
}
attr_str := if attrs.len > 0 { '(${attrs.join(' ')})' } else { '()' }
self.conn.write('* LIST ${attr_str} "/" "${pattern}"\r\n'.bytes())!
}
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,33 @@
module imap
import net
// handle_login processes the LOGIN command
// See RFC 3501 Section 6.2.3
pub fn (mut self Session) handle_login(tag string, parts []string) ! {
// Check if LOGINDISABLED is advertised
if self.capabilities.contains('LOGINDISABLED') {
self.conn.write('${tag} NO [PRIVACYREQUIRED] LOGIN disabled\r\n'.bytes())!
return
}
if parts.len < 4 {
self.conn.write('${tag} BAD LOGIN requires username and password\r\n'.bytes())!
return
}
username := parts[2]
password := parts[3]
// TODO: Implement actual authentication
// For demo purposes, accept any username/password
// In real implementation:
// 1. Validate credentials
// 2. If invalid, return: NO [AUTHENTICATIONFAILED] Authentication failed
// 3. If valid but can't authorize, return: NO [AUTHORIZATIONFAILED] Authorization failed
// After successful login:
// 1. Send capabilities in OK response
// 2. Don't include LOGINDISABLED or STARTTLS in capabilities after login
self.conn.write('${tag} OK [CAPABILITY IMAP4rev2 AUTH=PLAIN] LOGIN completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,9 @@
module imap
import net
// handle_logout processes the LOGOUT command
pub fn (mut self Session) handle_logout(tag string) ! {
self.conn.write('* BYE IMAP4rev2 Server logging out\r\n'.bytes())!
self.conn.write('${tag} OK LOGOUT completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,53 @@
module imap
import net
// handle_select processes the SELECT command
// See RFC 3501 Section 6.3.2
pub fn (mut self Session) handle_select(tag string, parts []string) ! {
if parts.len < 3 {
self.conn.write('${tag} BAD SELECT requires a mailbox name\r\n'.bytes())!
return error('SELECT requires a mailbox name')
}
// If there's a currently selected mailbox, send CLOSED response
if self.mailbox != '' {
self.conn.write('* OK [CLOSED] Previous mailbox is now closed\r\n'.bytes())!
}
// Remove any surrounding quotes from mailbox name
mailbox_name := parts[2].trim('"')
// Look for the mailbox
if mailbox_name !in self.server.mailboxes {
self.conn.write('${tag} NO Mailbox does not exist\r\n'.bytes())!
return error('Mailbox does not exist')
}
mut mbox := self.server.mailboxes[mailbox_name]
messages_count := mbox.messages.len
// Required untagged responses per spec:
// 1. FLAGS - list of flags that can be set on messages
self.conn.write('* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n'.bytes())!
// 2. EXISTS - number of messages
self.conn.write('* ${messages_count} EXISTS\r\n'.bytes())!
// Required OK untagged responses:
// 1. PERMANENTFLAGS
self.conn.write('* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted\r\n'.bytes())!
// 2. UIDNEXT
self.conn.write('* OK [UIDNEXT ${mbox.next_uid}] Predicted next UID\r\n'.bytes())!
// 3. UIDVALIDITY
self.conn.write('* OK [UIDVALIDITY ${mbox.uid_validity}] UIDs valid\r\n'.bytes())!
// Update session's selected mailbox
self.mailbox = mailbox_name
// Send READ-WRITE or READ-ONLY status in tagged response
// TODO: Implement proper access rights checking
self.conn.write('${tag} OK [READ-WRITE] SELECT completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,88 @@
module imap
import net
import strconv
// handle_store processes the STORE command
// See RFC 3501 Section 6.4.6
pub fn (mut self Session) handle_store(tag string, parts []string) ! {
mut mailbox := self.mailbox()!
// Expecting format like: A003 STORE sequence-set operation flags
if parts.len < 5 {
self.conn.write('${tag} BAD STORE requires a sequence-set, an operation, and flags\r\n'.bytes())!
return
}
// Parse sequence set (currently only supporting single message numbers)
sequence := parts[2]
index := strconv.atoi(sequence) or {
self.conn.write('${tag} BAD Invalid sequence-set\r\n'.bytes())!
return
} - 1
if index < 0 || index >= mailbox.messages.len {
self.conn.write('${tag} NO No such message\r\n'.bytes())!
return
}
// Parse operation (FLAGS, +FLAGS, -FLAGS, with optional .SILENT)
op := parts[3]
silent := op.ends_with('.SILENT')
base_op := if silent { op[..op.len-7] } else { op }
if base_op !in ['FLAGS', '+FLAGS', '-FLAGS'] {
self.conn.write('${tag} BAD Unknown STORE operation\r\n'.bytes())!
return
}
// Parse flags
flags_str := parts[4]
flags_clean := flags_str.trim('()')
flags_arr := flags_clean.split(' ').filter(it != '')
// Validate flags
valid_flags := ['\\Answered', '\\Flagged', '\\Deleted', '\\Seen', '\\Draft']
for flag in flags_arr {
if !flag.starts_with('\\') || flag !in valid_flags {
self.conn.write('${tag} BAD Invalid flag\r\n'.bytes())!
return
}
}
mut msg := mailbox.messages[index]
old_flags := msg.flags.clone() // Save for comparison
// Apply flag changes
match base_op {
'+FLAGS' {
// Add each flag if it isn't already present
for flag in flags_arr {
if flag !in msg.flags {
msg.flags << flag
}
}
}
'-FLAGS' {
// Remove specified flags
for flag in flags_arr {
msg.flags = msg.flags.filter(it != flag)
}
}
'FLAGS' {
// Replace current flags
msg.flags = flags_arr
}
else {}
}
// Save the updated message
mailbox.messages[index] = msg
// Send untagged FETCH response if flags changed and not silent
if !silent && msg.flags != old_flags {
self.conn.write('* ${index + 1} FETCH (FLAGS (${msg.flags.join(' ')}))\r\n'.bytes())!
}
self.conn.write('${tag} OK STORE completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,53 @@
module imap
import net
// handle_uid processes the UID command
// See RFC 3501 Section 6.4.9
pub fn (mut self Session) handle_uid(tag string, parts []string) ! {
if parts.len < 3 {
self.conn.write('${tag} BAD UID requires a command\r\n'.bytes())!
return
}
subcmd := parts[2].to_upper()
match subcmd {
'FETCH' {
// Remove 'UID' from parts and pass to handle_fetch
// The handle_fetch implementation already includes UIDs in responses
mut fetch_parts := parts.clone()
fetch_parts.delete(1) // Remove 'UID'
self.handle_fetch(tag, fetch_parts)!
}
'SEARCH' {
// Remove 'UID' from parts and pass to handle_search
mut search_parts := parts.clone()
search_parts.delete(1) // Remove 'UID'
// TODO: Implement handle_search
self.conn.write('${tag} NO SEARCH not implemented\r\n'.bytes())!
}
'STORE' {
// Remove 'UID' from parts and pass to handle_store
mut store_parts := parts.clone()
store_parts.delete(1) // Remove 'UID'
self.handle_store(tag, store_parts)!
}
'COPY' {
// Remove 'UID' from parts and pass to handle_copy
mut copy_parts := parts.clone()
copy_parts.delete(1) // Remove 'UID'
// TODO: Implement handle_copy
self.conn.write('${tag} NO COPY not implemented\r\n'.bytes())!
}
'EXPUNGE' {
// Remove 'UID' from parts and pass to handle_expunge
mut expunge_parts := parts.clone()
expunge_parts.delete(1) // Remove 'UID'
// TODO: Implement handle_expunge
self.conn.write('${tag} NO EXPUNGE not implemented\r\n'.bytes())!
}
else {
self.conn.write('${tag} BAD Unknown UID command\r\n'.bytes())!
}
}
}

View File

@@ -1,28 +1,38 @@
module imap
import time
pub fn start()! {
// Create the server and initialize an example INBOX.
mut server := IMAPServer{
mailboxes: map[string]&Mailbox{}
}
// Initialize INBOX with required IMAP4rev2 fields
mut inbox := Mailbox{
name: 'INBOX'
next_uid: 3 // Since we have 2 messages
uid_validity: u32(time.now().unix()) // Use current time as validity
read_only: false
messages: [
Message{
id: 1
uid: 1
subject: 'Welcome'
body: 'Welcome to the IMAP server!'
flags: ['\\Seen']
internal_date: time.now()
},
Message{
id: 2
uid: 2
subject: 'Update'
body: 'This is an update.'
flags: []
internal_date: time.now()
},
]
}
// Store a pointer to the INBOX.
server.mailboxes['INBOX'] = &inbox

View File

@@ -0,0 +1,35 @@
module imap
import net
import io
import time
// Our in-memory server holds a map of mailbox names to pointers to Mailbox.
@[heap]
pub struct IMAPServer {
pub mut:
mailboxes map[string]&Mailbox
}
pub struct Session {
pub mut:
server &IMAPServer
mailbox string // The name of the mailbox
conn net.TcpConn
reader &io.BufferedReader
tls_active bool // Whether TLS is active on the connection
capabilities []string // Current capabilities for this session
}
pub fn (mut self Session) mailbox_new(name string) !&Mailbox {
self.server.mailboxes[name] = &Mailbox{name:name}
return self.server.mailboxes[name]
}
pub fn (mut self Session) mailbox() !&Mailbox {
if !(self.mailbox in self.server.mailboxes) {
return error("mailbox ${self.mailbox} does not exist")
}
return self.server.mailboxes[self.mailbox] or { panic("bug") }
}

View File

@@ -40,7 +40,7 @@ fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
defer {
conn.close() or { panic(err) }
}
conn.write('* OK [CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS LOGIN] IMAP server ready\r\n'.bytes())!
conn.write('* OK [CAPABILITY IMAP4rev2 STARTTLS LOGINDISABLED AUTH=PLAIN] IMAP server ready\r\n'.bytes())!
// Initially no mailbox is selected.
mut selected_mailbox_name := ''
mut res := false
@@ -54,10 +54,12 @@ fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
}
mut session:= Session{
server:&server
mailbox:""
conn:conn
reader:reader
server: &server
mailbox: ""
conn: conn
reader: reader
tls_active: false
capabilities: ['IMAP4rev2', 'STARTTLS', 'LOGINDISABLED', 'AUTH=PLAIN']
}
for {
@@ -99,19 +101,28 @@ fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
session.handle_authenticate(tag, parts)!
}
'SELECT' {
session.selected_mailbox_name = handle_select(mut conn, tag, parts, mut server) !
session.handle_select(tag, parts)!
}
'FETCH' {
session.handle_fetch(mut conn, tag, parts) !
session.handle_fetch(tag, parts) !
}
'STORE' {
session.handle_store(mut conn, tag, parts) !
session.handle_store(tag, parts) !
}
'CAPABILITY' {
session.handle_capability(mut conn, tag) !
session.handle_capability(tag) !
}
'LIST' {
session.handle_list(tag, parts) !
}
'UID' {
session.handle_uid(tag, parts) !
}
'CLOSE' {
session.handle_close(tag) !
}
'LOGOUT' {
handle_logout(mut conn, tag) !
session.handle_logout(tag) !
return
}
else {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
# Mailbox Module
A V language implementation of a mailbox system that provides core functionality for managing email messages. This module is designed to be used as part of an email server implementation, providing the fundamental storage and retrieval operations for email messages.
## Features
- Message management with unique identifiers (UIDs)
- CRUD operations for messages (Create, Read, Update, Delete)
- Message searching capabilities
- Support for message flags (e.g., \Seen, \Flagged)
- Read-only mailbox support
## Core Components
### Message
```v
pub struct Message {
pub mut:
uid u32 // Unique identifier for the message
subject string
body string
flags []string // e.g.: ["\Seen", "\Flagged"]
internal_date time.Time // Message arrival time
}
```
### Mailbox
```v
pub struct Mailbox {
pub mut:
name string
messages []Message
next_uid u32 // Next unique identifier to be assigned
uid_validity u32 // Unique identifier validity value
read_only bool // Whether mailbox is read-only
}
```
## Usage Examples
### Basic Operations
```v
// Create a new mailbox
mut mb := Mailbox{
name: 'INBOX'
next_uid: 1
uid_validity: 1
}
// Add a message
msg := Message{
uid: 1
subject: 'Hello'
body: 'World'
flags: ['\Seen']
}
mb.set(msg.uid, msg)!
// Get a message
found_msg := mb.get(1)!
// List all messages
messages := mb.list()!
// Delete a message
mb.delete(1)!
```
### Searching Messages
```v
// Search for messages with specific criteria
results := mb.find(FindArgs{
subject: 'Hello'
content: 'World'
flags: ['\Seen']
})!
```
## Notes
- Each message has a unique identifier (UID) that remains constant
- The `uid_validity` value helps clients detect mailbox changes
- Messages can be flagged with standard IMAP flags
- Search operations support filtering by subject, content, and flags

View File

@@ -0,0 +1,47 @@
module mailbox
import time
// Creates demo data with 5 user accounts, each having 2 mailboxes and 20 messages
pub fn (mut self MailServer) demodata() ! {
usernames := ['user1', 'user2', 'user3', 'user4', 'user5']
names := ['First User', 'Second User', 'Third User', 'Fourth User', 'Fifth User']
for i, username in usernames {
// Create primary and alternate email addresses
primary_email := '${username}@example.com'
alt_email := '${username}.alt@example.com'
emails := [primary_email, alt_email]
// Create user account
mut account := self.create_account(username, names[i], emails) or { return err }
// Create second mailbox (INBOX is created by default)
account.create_mailbox('Sent') or { return err }
// Get both mailboxes
mut inbox := account.get_mailbox('INBOX') or { return err }
mut sent := account.get_mailbox('Sent') or { return err }
// Add 10 messages to each mailbox
for j in 0..10 {
// Add message to INBOX
inbox_msg := Message{
uid: inbox.next_uid + u32(j)
subject: 'Inbox Message ${j + 1}'
body: 'This is inbox message ${j + 1} for ${username}'
flags: if j % 2 == 0 { ['\\Seen'] } else { [] }
internal_date: time.now()
}
inbox.set(inbox_msg.uid, inbox_msg) or { return err }
// Add message to Sent
sent_msg := Message{
uid: sent.next_uid + u32(j)
subject: 'Sent Message ${j + 1}'
body: 'This is sent message ${j + 1} from ${username}'
flags: ['\\Seen']
internal_date: time.now()
}
sent.set(sent_msg.uid, sent_msg) or { return err }
}
}
}

View File

@@ -0,0 +1,64 @@
module mailbox
fn new_mailserver() &MailServer {
return &MailServer{
accounts: map[string]&UserAccount{}
}
}
fn test_demodata() ! {
mut server := new_mailserver()
server.demodata()!
// Test user accounts
usernames := ['user1', 'user2', 'user3', 'user4', 'user5']
names := ['First User', 'Second User', 'Third User', 'Fourth User', 'Fifth User']
for i, username in usernames {
// Verify user account exists and properties are correct
mut account := server.get_account(username)!
assert account.name == username
assert account.description == names[i]
assert account.emails.len == 2
assert account.emails[0] == '${username}@example.com'
assert account.emails[1] == '${username}.alt@example.com'
// Verify mailboxes exist
mailboxes := account.list_mailboxes()
assert mailboxes.len == 2
assert mailboxes.contains('INBOX')
assert mailboxes.contains('Sent')
// Verify INBOX messages
mut inbox := account.get_mailbox('INBOX')!
messages := inbox.list()!
assert messages.len == 10
// Check specific properties of first and last INBOX messages
first_msg := inbox.get(messages[0].uid)!
assert first_msg.subject == 'Inbox Message 1'
assert first_msg.body == 'This is inbox message 1 for ${username}'
assert first_msg.flags == ['\\Seen']
last_msg := inbox.get(messages[9].uid)!
assert last_msg.subject == 'Inbox Message 10'
assert last_msg.body == 'This is inbox message 10 for ${username}'
assert last_msg.flags == if 9 % 2 == 0 { ['\\Seen'] } else { [] }
// Verify Sent messages
mut sent := account.get_mailbox('Sent')!
sent_messages := sent.list()!
assert sent_messages.len == 10
// Check specific properties of first and last Sent messages
first_sent := sent.get(sent_messages[0].uid)!
assert first_sent.subject == 'Sent Message 1'
assert first_sent.body == 'This is sent message 1 from ${username}'
assert first_sent.flags == ['\\Seen']
last_sent := sent.get(sent_messages[9].uid)!
assert last_sent.subject == 'Sent Message 10'
assert last_sent.body == 'This is sent message 10 from ${username}'
assert last_sent.flags == ['\\Seen']
}
}

View File

@@ -0,0 +1,104 @@
module mailbox
// Represents a mailbox holding messages.
@[heap]
pub struct Mailbox {
pub mut:
name string
messages []Message
next_uid u32 // Next unique identifier to be assigned
uid_validity u32 // Unique identifier validity value
read_only bool // Whether mailbox is read-only
}
// Returns all messages in the mailbox
pub fn (mut self Mailbox) list() ![]Message {
return self.messages
}
// Gets a message by its UID
pub fn (mut self Mailbox) get(uid u32) !Message {
for msg in self.messages {
if msg.uid == uid {
return msg
}
}
return error('Message with UID ${uid} not found')
}
// Deletes a message by its UID
pub fn (mut self Mailbox) delete(uid u32) ! {
for i, msg in self.messages {
if msg.uid == uid {
self.messages.delete(i)
return
}
}
return error('Message with UID ${uid} not found')
}
// Sets/updates a message with the given UID
pub fn (mut self Mailbox) set(uid u32, msg Message) ! {
if self.read_only {
return error('Mailbox is read-only')
}
mut found := false
for i, existing in self.messages {
if existing.uid == uid {
self.messages[i] = msg
found = true
break
}
}
if !found {
// Add as new message if UID doesn't exist
self.messages << msg
}
}
@[params]
pub struct FindArgs{
pub mut:
subject string
content string
flags []string
}
// Finds messages matching the given criteria
pub fn (mut self Mailbox) find(args FindArgs) ![]Message {
mut results := []Message{}
for msg in self.messages {
mut matches := true
// Check subject if specified
if args.subject != '' && !msg.subject.contains(args.subject) {
matches = false
}
// Check content if specified
if matches && args.content != '' && !msg.body.contains(args.content) {
matches = false
}
// Check all specified flags are present
if matches && args.flags.len > 0 {
for flag in args.flags {
if flag !in msg.flags {
matches = false
break
}
}
}
if matches {
results << msg
}
}
return results
}

View File

@@ -0,0 +1,142 @@
module mailbox
import time
fn test_mailbox_basic_operations() {
mut mb := Mailbox{
name: 'INBOX'
uid_validity: 1234
}
// Test empty mailbox
msgs := mb.list() or { panic(err) }
assert msgs.len == 0
// Test adding a message
msg1 := Message{
uid: 1
subject: 'Test email'
body: 'Hello world'
flags: ['\\Seen']
internal_date: time.now()
}
mb.set(1, msg1) or { panic(err) }
// Test listing messages
msgs2 := mb.list() or { panic(err) }
assert msgs2.len == 1
assert msgs2[0].subject == 'Test email'
// Test getting message by UID
found := mb.get(1) or { panic(err) }
assert found.uid == 1
assert found.subject == 'Test email'
assert found.body == 'Hello world'
assert found.flags == ['\\Seen']
}
fn test_mailbox_delete() {
mut mb := Mailbox{
name: 'INBOX'
uid_validity: 1234
}
// Add two messages
msg1 := Message{
uid: 1
subject: 'First email'
body: 'Content 1'
}
msg2 := Message{
uid: 2
subject: 'Second email'
body: 'Content 2'
}
mb.set(1, msg1) or { panic(err) }
mb.set(2, msg2) or { panic(err) }
// Delete first message
mb.delete(1) or { panic(err) }
// Verify only second message remains
msgs := mb.list() or { panic(err) }
assert msgs.len == 1
assert msgs[0].uid == 2
assert msgs[0].subject == 'Second email'
// Test deleting non-existent message
if _ := mb.delete(999) {
panic('Expected error when deleting non-existent message')
}
}
fn test_mailbox_find() {
mut mb := Mailbox{
name: 'INBOX'
uid_validity: 1234
}
// Add test messages
msg1 := Message{
uid: 1
subject: 'Important meeting'
body: 'Meeting at 2 PM'
flags: ['\\Seen', '\\Flagged']
}
msg2 := Message{
uid: 2
subject: 'Hello friend'
body: 'How are you?'
flags: ['\\Seen']
}
msg3 := Message{
uid: 3
subject: 'Another meeting'
body: 'Team sync at 3 PM'
flags: ['\\Draft']
}
mb.set(1, msg1) or { panic(err) }
mb.set(2, msg2) or { panic(err) }
mb.set(3, msg3) or { panic(err) }
// Test finding by subject
found_subject := mb.find(FindArgs{subject: 'meeting'}) or { panic(err) }
assert found_subject.len == 2
// Test finding by content
found_content := mb.find(FindArgs{content: 'PM'}) or { panic(err) }
assert found_content.len == 2
// Test finding by flags
found_flags := mb.find(FindArgs{flags: ['\\Seen', '\\Flagged']}) or { panic(err) }
assert found_flags.len == 1
assert found_flags[0].uid == 1
// Test finding with multiple criteria
found_multi := mb.find(FindArgs{
subject: 'meeting'
flags: ['\\Draft']
}) or { panic(err) }
assert found_multi.len == 1
assert found_multi[0].uid == 3
}
fn test_readonly_mailbox() {
mut mb := Mailbox{
name: 'INBOX'
uid_validity: 1234
read_only: true
}
msg := Message{
uid: 1
subject: 'Test email'
body: 'Hello world'
}
// Attempt to modify read-only mailbox should fail
if _ := mb.set(1, msg) {
panic('Expected error when modifying read-only mailbox')
}
}

View File

@@ -0,0 +1,69 @@
module mailbox
import time
// Represents the mail server that manages user accounts
@[heap]
pub struct MailServer {
pub mut:
accounts map[string]&UserAccount // Map of username to user account
}
// Creates a new user account
pub fn (mut self MailServer) create_account(username string, description string, emails []string) !&UserAccount {
if username in self.accounts {
return error('User ${username} already exists')
}
// Verify emails are unique across all accounts
for _, account in self.accounts {
for email in emails {
if email in account.emails {
return error('Email ${email} is already registered to another account')
}
}
}
mut account := &UserAccount{
name: username
description: description
emails: emails.clone()
mailboxes: map[string]&Mailbox{}
}
self.accounts[username] = account
// Create default INBOX mailbox
account.create_mailbox('INBOX') or { return err }
return account
}
// Gets a user account by username
pub fn (mut self MailServer) get_account(username string) !&UserAccount {
if account := self.accounts[username] {
return account
}
return error('User ${username} not found')
}
// Deletes a user account
pub fn (mut self MailServer) delete_account(username string) ! {
if username !in self.accounts {
return error('User ${username} not found')
}
self.accounts.delete(username)
}
// Lists all usernames
pub fn (self MailServer) list_accounts() []string {
return self.accounts.keys()
}
// Finds account by email address
pub fn (mut self MailServer) find_account_by_email(email string) !&UserAccount {
for _, account in self.accounts {
if email in account.emails {
return account
}
}
return error('No account found with email ${email}')
}

View File

@@ -0,0 +1,13 @@
module mailbox
import time
// Represents an email message.
pub struct Message {
pub mut:
uid u32 // Unique identifier for the message
subject string
body string
flags []string // e.g.: ["\\Seen", "\\Flagged"]
internal_date time.Time // Message arrival time
}

View File

@@ -0,0 +1,47 @@
module mailbox
import time
// Represents a user account in the mail server
@[heap]
pub struct UserAccount {
pub mut:
name string
description string
emails []string
mailboxes map[string]&Mailbox // Map of mailbox name to mailbox instance
}
// Creates a new mailbox for the user account
pub fn (mut self UserAccount) create_mailbox(name string) !&Mailbox {
if name in self.mailboxes {
return error('Mailbox ${name} already exists')
}
mb := &Mailbox{
name: name
uid_validity: u32(time.now().unix())
}
self.mailboxes[name] = mb
return mb
}
// Gets a mailbox by name
pub fn (mut self UserAccount) get_mailbox(name string) !&Mailbox {
if mailbox := self.mailboxes[name] {
return mailbox
}
return error('Mailbox ${name} not found')
}
// Deletes a mailbox by name
pub fn (mut self UserAccount) delete_mailbox(name string) ! {
if name !in self.mailboxes {
return error('Mailbox ${name} not found')
}
self.mailboxes.delete(name)
}
// Lists all mailboxes for the user
pub fn (self UserAccount) list_mailboxes() []string {
return self.mailboxes.keys()
}

View File

@@ -0,0 +1,134 @@
module mailbox
import time
fn test_user_account_mailboxes() {
mut account := UserAccount{
name: 'testuser'
description: 'Test User'
emails: ['test@example.com']
}
// Test creating mailboxes
inbox := account.create_mailbox('INBOX') or { panic(err) }
assert inbox.name == 'INBOX'
sent := account.create_mailbox('Sent') or { panic(err) }
assert sent.name == 'Sent'
// Test duplicate mailbox creation
if _ := account.create_mailbox('INBOX') {
panic('Expected error when creating duplicate mailbox')
}
// Test listing mailboxes
boxes := account.list_mailboxes()
assert boxes.len == 2
assert 'INBOX' in boxes
assert 'Sent' in boxes
// Test getting mailbox
found := account.get_mailbox('INBOX') or { panic(err) }
assert found.name == 'INBOX'
// Test getting non-existent mailbox
if _ := account.get_mailbox('NonExistent') {
panic('Expected error when getting non-existent mailbox')
}
// Test deleting mailbox
account.delete_mailbox('Sent') or { panic(err) }
boxes_after_delete := account.list_mailboxes()
assert boxes_after_delete.len == 1
assert 'Sent' !in boxes_after_delete
}
fn test_mail_server_accounts() {
mut server := MailServer{}
// Test creating accounts
mut account1 := server.create_account('user1', 'First User', ['user1@example.com', 'user1.alt@example.com']) or { panic(err) }
assert account1.name == 'user1'
assert account1.emails.len == 2
// Verify INBOX was created automatically
mut inbox := account1.get_mailbox('INBOX') or { panic(err) }
assert inbox.name == 'INBOX'
// Test creating account with duplicate username
if _ := server.create_account('user1', 'Duplicate User', ['other@example.com']) {
panic('Expected error when creating account with duplicate username')
}
// Test creating account with duplicate email
if _ := server.create_account('user2', 'Second User', ['user1@example.com']) {
panic('Expected error when creating account with duplicate email')
}
// Test creating another valid account
mut account2 := server.create_account('user2', 'Second User', ['user2@example.com']) or { panic(err) }
assert account2.name == 'user2'
// Test listing accounts
accounts := server.list_accounts()
assert accounts.len == 2
assert 'user1' in accounts
assert 'user2' in accounts
// Test getting account
mut found := server.get_account('user1') or { panic(err) }
assert found.name == 'user1'
assert found.emails == ['user1@example.com', 'user1.alt@example.com']
// Test getting non-existent account
if _ := server.get_account('nonexistent') {
panic('Expected error when getting non-existent account')
}
// Test finding account by email
mut found_by_email := server.find_account_by_email('user1.alt@example.com') or { panic(err) }
assert found_by_email.name == 'user1'
// Test finding non-existent email
if _ := server.find_account_by_email('nonexistent@example.com') {
panic('Expected error when finding non-existent email')
}
// Test deleting account
server.delete_account('user2') or { panic(err) }
accounts_after_delete := server.list_accounts()
assert accounts_after_delete.len == 1
assert 'user2' !in accounts_after_delete
}
fn test_end_to_end() {
mut server := MailServer{}
// Create account
mut account := server.create_account('testuser', 'Test User', ['test@example.com']) or { panic(err) }
// Get INBOX and add a message
mut inbox := account.get_mailbox('INBOX') or { panic(err) }
msg := Message{
uid: 1
subject: 'Test message'
body: 'Hello world'
flags: ['\\Seen']
}
inbox.set(1, msg) or { panic(err) }
// Create Archives mailbox
mut archives := account.create_mailbox('Archives') or { panic(err) }
// Verify mailboxes through server lookup
mut found_account := server.get_account('testuser') or { panic(err) }
mailboxes := found_account.list_mailboxes()
assert mailboxes.len == 2
assert 'INBOX' in mailboxes
assert 'Archives' in mailboxes
// Verify message in INBOX
mut found_inbox := found_account.get_mailbox('INBOX') or { panic(err) }
msgs := found_inbox.list() or { panic(err) }
assert msgs.len == 1
assert msgs[0].subject == 'Test message'
}