feat(fls): import the e-mail programs into the editor model

The e-mail client and server of every host come into the model (core:
EmailClientConfiguration, EmailServerConfiguration, EmailMessage), from
Java's view:

- the server's accounts are those of /mailserver/konten.txt, as Java reads
  them at every simulation start; without the file those of the bean list,
  which Java would drop;
- the client's account is that of /konten.txt; without it the first of an
  older file's kontoListe, which FILIUS 2.14 no longer reads;
- messages are numbered per program in folder order, as Swift does; the
  sent folder's are sent; a date is taken only when the file gives one.

Departures from Swift: accounts and messages stay where Swift drops what
fails its checks; first and last name stay apart; a port that is no number
is the default, with a warning. On the corpus the model matches Swift's on
all 13 files; the one Swift error is named (it numbers a mailbox's messages
as their received time and writes that into the konten.txt it rewrites).

The Swift dump now carries the e-mail state, and the file system
comparison leaves out only Swift's hidden state files (and the directories
it makes for them) instead of all of /mail, /mailserver and /peer2peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 07:36:05 +00:00
co-authored by Claude Opus 5.5
parent 8f9930f1ff
commit 1efbb8a660
11 changed files with 1404 additions and 27 deletions
@@ -404,6 +404,30 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON
"records": dns.typedRecords.map { dnsRecordJSON($0) },
] as JSON
}
if let client = state.runtimeEmailClientConfigurationsByNodeID[node.id] {
json["emailClient"] = [
"pop3Host": client.pop3Host, "pop3Port": client.pop3Port,
"smtpHost": client.smtpHost, "smtpPort": client.smtpPort,
"username": client.username, "password": client.password,
"name": client.name, "email": client.email,
"inbox": client.inbox.map(emailMessageJSON),
"sent": client.sent.map(emailMessageJSON),
"drafts": client.drafts.map(emailMessageJSON),
"nextMessageID": Int(client.nextMessageID),
] as JSON
}
if let server = state.runtimeEmailServerConfigurationsByNodeID[node.id] {
json["emailServer"] = [
"domain": server.domain, "pop3Port": server.pop3Port,
"accounts": server.accounts.map { account in
[
"username": account.username, "password": account.password, "name": account.name,
"mailbox": account.mailbox.map(emailMessageJSON),
] as JSON
},
"nextMessageID": Int(server.nextMessageID),
] as JSON
}
if let link = state.remoteLinkConfigurationsByNodeID[node.id] {
json["remoteLink"] = [
"pairIdentifier": link.pairIdentifier,
@@ -615,6 +639,27 @@ func foldPaths(_ cases: [PathCase]) -> [JSON] {
}
}
func emailAddressJSON(_ address: TopologyRuntimeEmailAddress) -> [Any] {
[address.name ?? NSNull(), address.mailAddress]
}
func emailMessageJSON(_ message: TopologyRuntimeEmailMessage) -> JSON {
[
"id": Int(message.id),
"from": emailAddressJSON(message.from),
"to": message.to.map(emailAddressJSON),
"cc": message.cc.map(emailAddressJSON),
"bcc": message.bcc.map(emailAddressJSON),
"subject": message.subject,
"body": message.body,
"deliveryIdentity": message.deliveryIdentity ?? NSNull(),
"receivedAtMilliseconds": message.receivedAtMilliseconds.map { Int($0) } ?? NSNull(),
"isNew": message.isNew,
"isMarkedForDeletion": message.isMarkedForDeletion,
"isSent": message.isSent,
]
}
// MARK: - /dns/hosts cases (packages/fls/test/dns-hosts.cases.txt)
func dnsRecordJSON(_ record: TopologyDNSResourceRecord) -> [Any] {