ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Thunderbird「Export for Mobile」二维码数据格式规范(Version 1)深度解析

Thunderbird「Export for Mobile」二维码数据格式规范(Version 1)深度解析 移动开发企业应用【免费下载链接】thunderbird-androidThunderbird for Android – Open Source Email App for Android (fka K-9 Mail)项目地址https://gitcode.com/gh_mirrors/th/thunderbird-android点击查看免费下载导读本文完整解读 Thunderbird for Android前身 K-9 Mail与 Thunderbird 桌面版之间「扫码迁移账户」所依赖的二维码数据格式规范qr-code-format.md。该规范定义了桌面版将邮箱账户收件服务器、发件服务器与身份信息编码为 JSON 载荷、再以二维码形式交给移动端扫描解析的完整协议。读完本文你将掌握该格式的根元素结构、各字段枚举值与可选元素规则、多二维码序列SequenceNumber/SequenceEnd机制并能结合仓库源码理解 Thunderbird for Android 中从「扫描二维码」到「写入账户设置 XML」的完整实现链路。1. 规范背景与版本该规范由 feature/migration/qrcode/qr-code-format.md 定义其用途是描述Thunderbird 桌面版Writer将账户导出为二维码载荷、Thunderbird for AndroidReader扫描并解析该载荷的数据交换格式。规范当前只有一个版本Version 12025-02-06初始版本本文即基于此版本撰写。需要特别区分两个「版本」概念概念含义变更时机规范版本Specification Version本文档自身的版本2025-02-06为 v1规范文档更新时格式版本FormatVersion载荷中FormatVersion字段当前固定为1仅在数据格式发生向后不兼容变更时规范原文明确「Whenever this document refers to a version without qualifier, the specification version is meant. It is different from the format version.」即文档中不带限定词的「版本」均指规范版本而载荷中的FormatVersion预期在数据格式不出现不兼容变更前保持不变。2. 核心术语Terms规范为后续行文定义了一组严格术语Reader读取方扫描二维码后解析载荷的软件例如 Thunderbird for Android。Writer写入方按照本规范生成二维码的软件例如 Thunderbird 桌面版。Account账户在本规范语境下账户 一个IncomingServer对象 紧随其后的一个OutgoingServerGroups对象详见根元素。3. 总体结构与设计动机3.1 为什么是「JSON 嵌套数组」规范指出二维码载荷是UTF-8 编码的 JSON入站服务器、出站服务器、身份Identity等数据对象被映射为JSON 数组。选择这一组合的原因与代价如下优点JSON 是广泛支持的格式易于后续扩展新增属性方便使用嵌套 JSON 数组可以显著压缩输出体积而二维码的空间极其有限。缺点对实现者更不友好大多数面向对象语言都有「对象 ↔ JSON 对象」的映射库但「对象 ↔ JSON 数组」的库映射能力通常不具备JSON 数组对人类阅读不友好可读性差。规范原文甚至自嘲地总结道「This is not a great data format. But its one that seems to work well within the constraints of a QR code.」这不是一个优秀的数据格式但在二维码的约束下它工作得很好。3.2 前向兼容Forward Compatibility格式通过「数组可包含规范之外的元素」实现可扩展性此规则适用于所有数组除非另有说明Reader必须忽略其实现的规范版本中未列出的额外数组元素Writer不得添加其实现的规范版本之外的数组元素。在源码实现中这一规则被直接落实。见 QrCodePayloadAdapter.ktprivate fun skipAdditionalArrayEntries(jsonReader: JsonReader) { // For forward compatibility allow additional array elements. while (jsonReader.hasNext()) { jsonReader.readJsonValue() } }每个数组读取完毕后都会调用skipAdditionalArrayEntries将尚未消费的额外元素整体跳过从而保证未来规范追加元素时旧版 Reader 依然可以正确解析。3.3 兼容性要求Compatibility规范提出一条重要约束即使 Reader 不使用载荷中的全部信息也仍然必须校验所有属性包括未使用的属性。原因在于避免出现「一个不完整的Reader 认为二维码有效、而另一个完整的Reader 认为无效」的分歧场景。Thunderbird for Android 的实现正是如此——见下文第 6 节的QrCodePayloadValidator它会在映射前对全部字段做严格校验。3.4 多二维码机制Multiple QR codes数据格式支持在单个导出操作中编码不限数量的账户但单个二维码的载荷大小是有限的因此导出多个账户时可能需要生成多个二维码SequenceNumber/SequenceEnd机制见下文用于告知 Reader 一次导出包含几个二维码、以及当前的阅读顺序注意本格式不支持将一个账户的数据拆散到多个二维码中因此单个账户可编码的数据量受限于最大二维码容量规范注明实践中至今未成为问题具体的「每个二维码装几个账户」策略由 Writer 决定。由于大二维码更难扫描规范建议 Writer 以「目标最大二维码尺寸」为导向而非固定每个二维码放 N 个账户。4. 数据格式详解4.1 根元素Root element可用版本version 1类型ArrayJSON 文档的根元素是一个数组按以下顺序包含FormatVersionMiscellaneousDataIncomingServerOutgoingServerGroups数组还可以包含更多IncomingServer/OutgoingServerGroups元素但它们必须成对且按此顺序出现——只有两者组合在一起才构成一个完整账户。4.2FormatVersion类型Integer取值1数据格式的版本号。只有现有扩展机制不够用、需要做向后不兼容变更时才需要改变。规范只描述了格式版本 1若 Reader 遇到其他值必须显式支持该版本或报错失败。有意思的是规范还指出未来的新格式可能不再以 JSON 数组作为根元素那样的话即使不读取FormatVersion也能直接检测出不兼容。在 Thunderbird for Android 的读取链路中版本检查最先发生。见 QrCodePayloadAdapter.ktval version jsonReader.nextInt() if (version ! 1) { // We dont even attempt to read something that is newer than version 1. Log.d(Unsupported version: %s, version) return null }同时 QrCodePayloadValidator.kt 也会再次校验data.version ! 1即判定无效。4.3MiscellaneousData类型Array按顺序包含SequenceNumberSequenceEnd未来规范版本可能向该数组追加更多值Reader 需按前向兼容规则忽略。4.4SequenceNumber与SequenceEnd两者配合解决「一次导出拆成多个二维码」的问题字段类型含义SequenceNumberInteger当前二维码在序列中的从 1 开始的索引1-basedSequenceEndInteger一次导出操作总共使用的二维码数量Reader 结合两者即可判断已经读到序列中的第几个、还缺几个。例如[2, 3]表示「这是第 2 个、总共 3 个」。4.5IncomingServer与IncomingProtocolIncomingServer数组存放账户中只出现一次的信息主要是入站服务器信息。数组第一个元素是IncomingProtocol其取值决定数组其余部分的构成。类型Integer取值0IMAPversion 1 起1POP3version 1 起对于取值 0 和 1IncomingServer数组内容依次为IncomingProtocolHostnamePortConnectionSecurityAuthenticationTypeUsernameAccountName可选Password可选三条关键规则Reader 遇到不支持的IncomingProtocol值必须跳过整个账户其他属性含不支持的值时同样必须跳过该账户Writer 可以省略可选元素前提是后续元素也一并省略即不能留下「空洞」。因为数组大小取决于IncomingProtocol的值未来规范只能按协议粒度追加属性。4.6AccountName的写入与读取规则如果账户名等于第一个身份Identity的邮箱地址Writer 可以省略该值如果该元素无法省略因为其后还有元素存在如Password可以用null或空字符串代替Reader 在AccountName缺失、为null或空字符串时必须使用第一个身份的邮箱地址作为账户名。在实现中这一逻辑体现在 QrCodePayloadMapper.kt// When setting up an account in Thunderbird, the account name matches the email address. We can avoid this // duplication in the encoded data by omitting the account name when it matches the email address. return accountName?.takeIf { it.isNotEmpty() } ?: identity.emailAddress.toString()4.7Password的写入与读取规则Writer 若不想包含密码可省略Password元素为前向兼容Reader 必须把Password取值为null或空字符串视为「省略密码」同样处理。在 QrCodePayloadAdapter.kt 中入站密码通过if (jsonReader.hasNext()) jsonReader.nextString() else null读取即元素缺失时得到null与规范一致。4.8Hostname类型String服务器主机名。目前只允许 ASCII 主机名这包括国际化域名IDN的 ASCII 兼容编码ACE即 punycode。4.9Port类型Integer取值1~65535入站或出站服务器使用的 TCP 端口。4.10ConnectionSecurity类型Integer取值0Plain明文version 1 起1 遗留值不要使用version 1 起保留2AlwaysStartTlsversion 1 起3Tlsversion 1 起描述连接服务器时是否使用 TLS 以及如何使用。有趣的是源码对取值1的处理虽然规范将其标记为「legacy, do not use; reserved」但 Thunderbird for Android 的整数映射器 IntValueMapper.kt 仍将其按AlwaysStartTls处理并附带注释// TryStartTls, but we treat it like AlwaysStartTls也就是说读取侧把遗留的「尝试 STARTTLS」语义宽容地归并到了「始终 STARTTLS」上。4.11AuthenticationType类型Integer取值0None1PasswordCleartext明文密码2PasswordEncrypted加密密码3Gssapi4Ntlm5TlsCertificate6OAuth2以上全部自 version 1 起可用。认证方式与协议、服务器能力共同决定移动端如何建立会话。对照 AccountData.kt域模型中的枚举只保留当前实际支持的子集None、PasswordCleartext、PasswordEncrypted、TlsCertificate、OAuth2。而 IntValueMapper.kt 在遇到3Gssapi、4Ntlm时会直接抛出IllegalArgumentException(Unsupported authentication method: ...)随后被校验器捕获、判定载荷无效——这正是规范「遇到不支持值必须跳过账户」的落地实现。4.12Username类型String用于认证的用户名。4.13AccountName类型String账户名称。若值为null或空字符串Reader 必须使用第一个身份的邮箱地址作为账户名。规范还补充了一条容错设计Reader 必须使用它成功读到的第一个身份的邮箱地址。这可能导致 Reader 使用的账户名与 Writer 预期不同但规范认为「得到一个非预期的账户名」优于「因 Reader 不支持读取第一个身份而跳过整个账户」。4.14Password类型String用于认证的密码。4.15OutgoingServerGroups/OutgoingServerGroupOutgoingServerGroups类型Array包含一个或多个OutgoingServerGroup元素OutgoingServerGroup类型Array按顺序包含OutgoingServerIdentityOutgoingServerGroup数组可包含额外的Identity元素即一个出站服务器可对应多个身份。若 Reader 读取OutgoingServer或全部Identity元素失败则必须跳过整个OutgoingServerGroup。4.16OutgoingServer与OutgoingProtocol与入站侧对称OutgoingServer数组第一个元素是OutgoingProtocol类型Integer取值0SMTPversion 1 起对于取值 0OutgoingServer数组内容依次为OutgoingProtocolHostnamePortConnectionSecurityAuthenticationTypeUsernamePassword可选规则同样Reader 遇到不支持的OutgoingProtocol或其余属性含不支持值时必须跳过整个OutgoingServerGroupPassword的省略 /null/ 空字符串语义与入站侧完全一致。4.17Identity/EmailAddress/DisplayNameIdentity数组按顺序包含EmailAddressDisplayNameReader 在任何元素含不支持的值时必须跳过该身份未来规范可向该数组追加值。EmailAddressString用于发信时的邮箱地址目前只允许 ASCIIDisplayNameString用于发信时显示的名称。5. 官方示例解读5.1 示例一单个 IMAP 账户[ 1, [1, 1], [0, imap.domain.example, 993, 3, 1, userdomain.example], [ [ [0, smtp.domain.example, 465, 3, 1, userdomain.example], [userdomain.example, Jane Doe] ] ] ]逐层拆解格式版本1序列信息[1, 1]→ 第 1 个 / 共 1 个无需再扫其他二维码入站服务器[0, imap.domain.example, 993, 3, 1, userdomain.example]协议IMAP0主机名imap.domain.example端口993连接安全Tls3认证方式PasswordCleartext1用户名userdomain.example密码不包含账户名未显式给出 → 隐式取第一个身份邮箱userdomain.example出站服务器组[[0, smtp.domain.example, 465, 3, 1, userdomain.example], [userdomain.example, Jane Doe]]协议SMTP0主机名smtp.domain.example端口465安全 Tls认证 PasswordCleartext密码不包含身份邮箱userdomain.example显示名Jane Doe5.2 示例二两个 IMAP 账户跨两个二维码[ 1, [1, 2], [ 0, imap.company.example, 993, 3, 6, usercompany.example, usercompany.example, ], [ [ [0, smtp.company.example, 465, 3, 6, usercompany.example, ], [usercompany.example, Jane Doe] ] ], [ 0, imap.domain.example, 993, 3, 1, janedomain.example, Jane (Personal), ], [ [ [0, smtp.domain.example, 465, 3, 1, janedomain.example, ], [janedomain.example, Jane] ] ] ]序列信息[1, 2]→ 第 1 个 / 共 2 个还有 1 个二维码待扫账户 1companyIMAP主机imap.company.example端口993Tls认证OAuth26账户名显式给出usercompany.example出站 SMTP 同为 OAuth2身份Jane Doe usercompany.example。注意入站数组末尾的是空字符串密码按规范等同于省略账户 2domain / 个人IMAP主机imap.domain.example端口993Tls认证 PasswordCleartext1账户名显式给出Jane (Personal)身份为Jane janedomain.example。这个例子同时演示了多账户导出、显式/隐式账户名、OAuth2 与明文密码两种认证、以及空字符串密码的编码写法。6. Thunderbird for Android 侧的实现链路规范描述了数据格式本身而仓库中的feature/migration/qrcode模块给出了 Reader 端的完整实现。整体处理流水线如下扫描二维码 → QrCodeImageAnalysisProvider/QrCodeAnalyzer识别 → QrCodePayloadReaderuse case 入口 → QrCodePayloadParserMoshi 流式解析为 QrCodeData → QrCodePayloadAdapter手写 JsonAdapter含前向兼容跳过 → QrCodePayloadMapper校验 映射为 AccountData → QrCodePayloadValidator逐字段严格校验 → XmlSettingWriter将 AccountData 写入 settings XML → 导入账户6.1 数据结构QrCodeDataQrCodeData.kt 以 Kotlin data class 完整镜像规范中的嵌套数组结构internal data class QrCodeData( val version: Int, val misc: Misc, val accounts: ListAccount, ) { data class Misc(val sequenceNumber: Int, val sequenceEnd: Int) data class Account(val incomingServer: IncomingServer, val outgoingServers: ListOutgoingServer) data class IncomingServer(val protocol: Int, val hostname: String, val port: Int, val connectionSecurity: Int, val authenticationType: Int, val username: String, val accountName: String?, val password: String?) data class OutgoingServer(val protocol: Int, val hostname: String, val port: Int, val connectionSecurity: Int, val authenticationType: Int, val username: String, val password: String?, val identities: ListIdentity) data class Identity(val emailAddress: String, val displayName: String) }注意其中accountName、password均为可空类型恰好对应规范「可选元素」的语义账户列表accounts对应规范「根数组可含多个成对的 IncomingServer/OutgoingServerGroups」。6.2 解析器QrCodePayloadAdapterQrCodePayloadParser解析使用 Moshi 的手写JsonAdapterQrCodePayloadAdapter.kt按规范顺序逐字段手工读取先beginArray()读根数组第一个nextInt()是版本号非 1 直接返回null再读MiscellaneousData两个整数 跳过额外元素随后循环读取账户每轮readAccount依次读IncomingServer与OutgoingServerGroups直到jsonReader.hasNext()为假入站服务器读取时accountName/password用if (jsonReader.hasNext()) jsonReader.nextString() else null处理可选性每个数组读取结束都调用skipAdditionalArrayEntries实现前向兼容。QrCodePayloadParser.kt 负责包裹异常JsonDataException与IOException均被捕获并记日志、返回null。单元测试 QrCodePayloadParserTest.kt 覆盖了「单账户、单身份、无账户名、无密码」「带账户名」「带密码」「双账户」等多种载荷组合验证解析结果与期望的QrCodeData完全一致。6.3 校验器QrCodePayloadValidatorQrCodePayloadValidator.kt 落实规范「即使不使用也要校验全部属性」的要求逐层校验版本必须为 1账户数组、出站服务器组、身份列表均不得为空入站/出站协议、连接安全、认证方式通过IntValueMapper的toXxx()转换不支持的值抛IllegalArgumentException主机名、端口、邮箱地址分别通过toHostname()、toPort()、toUserEmailAddress()校验格式AccountName、Username、Password、DisplayName不得包含换行符isSingleLine检查防止注入类问题邮箱解析失败EmailAddressParserException会被包装为IllegalArgumentException。任何一项失败都会使整个载荷被判为无效从而保证「完整 Reader」与「不完整 Reader」对同一二维码的判定结果一致。6.4 映射器QrCodePayloadMapperQrCodePayloadMapper.kt 在校验通过后把QrCodeData映射为域模型AccountData账户名取accountName?.takeIf { it.isNotEmpty() } ?: identity.emailAddress隐式账户名规则依据入站协议IMAP/POP3通过DeletePolicyProvider确定删除策略DeletePolicy这属于移动端附加的默认策略、不来自二维码各类整型枚举协议/安全/认证经IntValueMapper转为强类型枚举。入口用例 QrCodePayloadReader.kt 将「解析 校验 映射」串联为read(payload): AccountData?。6.5 落地写入XmlSettingWriter解析并映射得到的AccountData最终交给 XmlSettingWriter.kt以XmlSerializer输出为账户设置 XML含根元素、accounts/account元素、uuid属性等UUID 由DefaultUuidGenerator生成参见同目录UuidGenerator.kt。源码注释也承认该写入逻辑与SettingsExporter存在重复未来计划抽象公共层。至此桌面版编码、移动端解码、再到落盘配置的完整闭环得以打通。7. 实现者的关键注意事项严格遵守元素顺序根数组与各子数组的元素顺序是协议的一部分Reader/Writer 都不能调整可选元素必须「尾随省略」省略AccountName/Password时不得留下空洞后续元素存在时必须用null或空字符串占位遇到未知值即跳过不支持的协议、认证、安全取值意味着跳过账户/组/身份而不是局部容忍不省略校验即使不用某些字段也要完整校验以保持各实现间的判定一致性多二维码场景Writer 依据SequenceNumber/SequenceEnd拆分导出Reader 应据此提示用户继续扫描剩余二维码字符集限制主机名与邮箱地址目前仅限 ASCIIIDN 需使用 ACE 编码。8. 小结qr-code-format.md是一份紧凑而严谨的跨端数据交换规范它以「UTF-8 JSON 嵌套数组」在二维码容量约束下实现账户数据的高密度编码以「额外数组元素忽略 未知取值跳过」保证前向兼容以SequenceNumber/SequenceEnd支持多二维码导出。Thunderbird for Android 的feature/migration/qrcode模块从QrCodePayloadAdapter手写 Moshi 适配器、QrCodePayloadValidator全量校验到XmlSettingWriter落盘写入逐条兑现了规范要求并配有完整单元测试是理解和实现同类「扫码迁移账户」协议的最佳参照。赞分享移动开发企业应用【免费下载链接】thunderbird-androidThunderbird for Android – Open Source Email App for Android (fka K-9 Mail)项目地址https://gitcode.com/gh_mirrors/th/thunderbird-android点击查看免费下载相关推荐加速Huggingface模型下载的终极方案hf-mirror-cli深度解析加速Huggingface模型下载的终极方案hf mirror cli深度解析 hf mirror cli 是一款专为国内开发者设计的Huggingface模深入解析ZLIB压缩数据格式规范RFC 1950深入解析ZLIB压缩数据格式规范RFC 1950 概述 ZLIB压缩数据格式规范RFC 1950定义了一种通用的无损压缩数据格式该格式具有平台无关性、数据工程.NET runtime 便携式 PDB 元数据格式全解Portable PDB v1.0 规范深度剖析.NET runtime 便携式 PDB 元数据格式全解Portable PDB v1.0 规范深度剖析 Portable PDBPortable Prog语言运行时标准库JIT编译编译器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表