使用本地消息传递通过 Delphi 从 Thunderbird 复制电子邮件时遇到编码问题

Encounter encoding problem while copying an email from Thunderbird via Delphi using native messaging

我正在使用本地消息传递 (following the ping pong example in python) 在 thunderbird 中编写一个插件来调用 Delphi 程序以将电子邮件复制为本地“.eml”文件。我面临的问题似乎是编码。此外,生成的文件在文件的开头和结尾包含双引号 ("") 以及转义双引号 (\")。我只想拥有 1 对 1 的副本而不更改其内容。

邮件内容示例:

"test"
€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ
éöàäèüâêû 

但是,在文件中,它看起来更像这样:

\"test\"
€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ
éöà äèüâêû

我可能已经找到了问题所在,这里有解释:

https://www.i18nqa.com/debug/utf8-debug.html

但是,我真的不知道如何调整我的代码来解决这个问题。

感谢您的帮助!

这是我的 background.js:

async function main() {
        messenger.menus.create({
            contexts : ["message_list"],
            id: "copy@mail.lu",
            onclick : passMsg,
            title: messenger.i18n.getMessage("lang.menuTitle")
        });
    }
    
    async function passMsg(OnClickData) {
        if (OnClickData.selectedMessages && OnClickData.selectedMessages.messages.length > 0) {
            let MessageHeader = OnClickData.selectedMessages.messages[0];
            let raw = await messenger.messages.getRaw(MessageHeader.id);
            let port=browser.runtime.connectNative("copymail");
    
            port.onMessage.addListener((message) => {
              port.disconnect();
            });
    
            port.postMessage(raw);
        } else {
            console.log("No message selected");
        }
    }
    main();

这是我的 Delphi 代码:

procedure WriteSTDInputToFile(const Filename: String);
    var
       Buffer:    array [0 .. 3] of Byte;
       msgLen:    LongInt;
       msg:       UTF8String;
       myFile:    TextFile;
       StdIn:     THandleStream;
       jsonValue: TJSONValue;
    begin
       StdIn  := THandleStream.Create(GetStdHandle(STD_INPUT_HANDLE));
    
       try
          msgLen    := 0;
          if StdIn.Read(Buffer, SizeOf(msgLen)) > 0 then
             msgLen := PLongInt(@Buffer)^;
    
          if msgLen > 0 then
          begin
             SetLength(msg, msgLen);
             StdIn.Read(PUTF8Char(msg)^, msgLen);
    
             if msg <> '' then
             begin
                AssignFile(myFile, Filename, CP_UTF8);
                ReWrite(myFile);
    
                jsonValue := TJSONObject.ParseJSONValue(msg);
    
                try
                   write(myFile, UTF8Encode(jsonValue.ToString));
                finally
                   jsonValue.Free;
                end;
    
                CloseFile(myFile);
    
             end;
          end;
    
       finally
          if Assigned(StdIn) then
             StdIn.Free;
       end;
    
    end;

生成的文件内容:

"X-MDAV-Result: clean
X-MDAV-Processed: mail.test.lu, Wed, 28 Oct 2020 08:13:22 +0100
X-Spam-Processed: mail.test.lu, Wed, 28 Oct 2020 08:13:22 +0100
Return-path: <copy@mail.lu>
X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on MAIL01E
X-Spam-Level: 
X-Spam-Status: No, score=0.7 required=10.0 tests=HTML_MESSAGE,MPART_ALT_DIFF
    shortcircuit=no autolearn=disabled version=3.4.2
Authentication-Results: test.lu;
    auth=pass (plain) smtp.auth=ascholtes@test.lu
Received: from [172.16.17.35] [(172.16.17.35)] by test.lu (172.31.3.6) with ESMTPSA id md50033234892.msg; 
    Wed, 28 Oct 2020 08:13:21 +0100
X-MDRemoteIP: 172.16.17.35
X-MDArrival-Date: Wed, 28 Oct 2020 08:13:21 +0100
X-Authenticated-Sender: ascholtes@test.lu
X-Rcpt-To: copy@mail.lu
X-MDRcpt-To: copy@mail.lu
X-Return-Path: copy@mail.lu
X-Envelope-From: copy@mail.lu
X-MDaemon-Deliver-To: ascholtes@test.lu
To: Ayuth Scholtes <copy@mail.lu>
From: Ayuth Scholtes <copy@mail.lu>
Subject: Test
Organization: CISS
Message-ID: <7eb36f7c-a7af-c272-c189-eded642c3e1c@test.lu>
Date: Wed, 28 Oct 2020 08:13:21 +0100
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101
 Thunderbird/68.10.0
MIME-Version: 1.0
Content-Type: multipart/alternative;
 boundary=\"------------6068A746223BB2C9F1771938\"
Content-Language: lb-LU

This is a multi-part message in MIME format.
--------------6068A746223BB2C9F1771938
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 8bit

|\"test\" â¬âÆââ¦â â¡Ëâ°Å â¹ÅŽâââââ¢ââËâ¢Å¡âºÅžŸ éöàäèüâêû|


--------------6068A746223BB2C9F1771938
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 8bit

<html>
  <head>

    <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
  </head>
  <body>
    <pre class=\"lang-pascal s-code-block hljs delphi\"><code>\"test\"
â¬âÆââ¦â â¡Ëâ°Å â¹ÅŽâââââ¢ââËâ¢Å¡âºÅžŸ
éöàäèüâêû</code></pre>
  </body>
</html>

--------------6068A746223BB2C9F1771938--
"

首先我要说的是,您使用 native messaging 在网络扩展 (Thunderbird add-on) 和本机应用程序之间传输数据方面做得很好。理解和设置它并不容易,但您设法传输了所需的数据,但您在问题中描述了一些小故障。

... the resulting file contains double quotes (") at the start and the end of the file as well as escaped double quotes (\")

在 add-on 中,您获取原始电子邮件数据作为字符串 - console.log(typeof raw) 给出 string,然后您将其传递给 port.postMessage. Although the documentation says it takes JSON object representing the message to send, but it seems to accept single string value which is valid JSON according to some standards. In Delphi code you receive the message via STDIN and parse it using TJSONObject.ParseJSONValue into TJSONValue. It will in fact create instance of TJSONString. You can verify that by examining the value of jsonValue.ClassName. The problem with quotes arises when you use jsonValue.ToString which returns quoted version of the string that is basically the same what you had before parsing. Use the Value 属性 到 return 原始字符串值。

单独使用 jsonValue.Value 无法帮助您解决编码问题。您从 e-mail 客户端获得的原始消息数据位于 EML format. It conforms to RFC-822 中,这意味着它是 ASCII 编码的,但它可以包含任意编码的消息部分(请参阅您自己的示例 EML)。由于您只想保存 EML 文件而不考虑任何编码,最好是传输 EML 的原始字节,但这 out-of-the-box Javascript 和本地消息传递 API。因此,我建议您将 Base64 编码的数据字符串发送到本机应用程序,在本机应用程序中将其解码为可以直接写入磁盘的原始字节。

要在 add-on 中将原始消息数据编码为 Base64 字符串,请使用函数 btoa:

port.postMessage(btoa(raw));

要在本机应用程序中接收消息,您可以执行以下操作:

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON, System.NetEncoding, Winapi.Windows;

procedure WriteSTDInputToFile(const FileName: string);
var
  StdIn: THandleStream;
  MsgLen: Cardinal;
  Data: TBytes;
  JSONValue: TJSONValue;
begin
  StdIn := THandleStream.Create(GetStdHandle(STD_INPUT_HANDLE));
  try
    StdIn.ReadBuffer(MsgLen, SizeOf(MsgLen));
    SetLength(Data, MsgLen);
    StdIn.ReadBuffer(Data, MsgLen);
    JSONValue := TJSONObject.ParseJSONValue(Data, 0);
    Data := TNetEncoding.Base64.DecodeStringToBytes(JSONValue.Value);
    TFile.WriteAllBytes(FileName, Data);
  finally
    StdIn.Free;
  end;
end;

注意对原始代码的一些改进:

  • 使用 Cardinal 类型 MsgLen。该协议定义输入的前 4 个字节指示以字节为单位的消息长度,表示为 32 位无符号整数。 Cardinal 是 Delphi 这种值的本机类型,或者您也可以使用 UInt32 别名。
  • 我使用ReadBuffer方法而不是Read方法从STDIN读取,这使得程序在遇到一些意外情况时崩溃。理想情况下,您应该处理这种情况,通过 STDOUT 发送错误消息作为响应,并在 add-on.
  • 中处理响应
  • 我不会将传统的 I/O 例程与流混合使用。我什至没有在我的代码中使用流来写入输出文件。由于来自 Syste.IOUtils.
  • File.WriteAllBytes,创建文件 one-liner
  • 我不检查 if Assigned(StdIn) then StdIn.Free;。这就是 Free 已经为你做的。

知道传入消息是引用的 Base64 编码字符串,可以省略 JSON 处理,因此代码变为:

procedure WriteSTDInputToFile(const FileName: string);
var
  StdIn: THandleStream;
  MsgLen: Cardinal;
  Msg: RawByteString;
  Data: TBytes;
begin
  StdIn := THandleStream.Create(GetStdHandle(STD_INPUT_HANDLE));
  try
    StdIn.ReadBuffer(MsgLen, SizeOf(MsgLen));
    StdIn.Seek(1, soFromCurrent); { skip double quote }
    SetLength(Msg, MsgLen - 2); { minus leading and trailing double quotes }
    StdIn.ReadBuffer(Msg[Low(Msg)], MsgLen);
    Data := TNetEncoding.Base64.DecodeStringToBytes(UTF8ToString(Msg));
    TFile.WriteAllBytes(FileName, Data);
  finally
    StdIn.Free;
  end;
end;