Python + Zeep にて、SOAP with Attachment (SwA) でファイルを送信する

前回、inline (Base64エンコード) にて、SOAPでファイルを送信してみました。
Python + Zeepにて、SOAPのinline(Base64エンコード)でファイルを送信する - メモ的な思考的な

 
今回は、SOAP with attachments (以降、SwA) でファイルを送信してみます。実装に誤りがあれば、ご指摘いただけるとありがたいです。

 
目次

 

SwAの種類

SwAの定義は

このSwAの内容は、要するに上記のようなMIMEマルチパートにおいて最初のパーツに本文となるSOAPメッセージを配置し、2つ目以降のパーツにはSOAPメッセージに添付されるデータを配置するための決まりごとだ。

http://www.atmarkit.co.jp/fdotnet/special/wse04/wse04_01.html

です。

ただ、調べた中だと、SwAに該当するものは3つありました。

いずれも MIMEマルチパートを使っていますが、細かな点で実装が異なりました。

 
以降、個人的な理解です。

 

swaRefの実装

swaRefは、SOAPエンベロープ

<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body>
    <ns0:RequestInterface xmlns:ns0="http://example.com/HelloWorld">
      <ns0:image href="cid:spam"/>
    </ns0:RequestInterface>
  </soap-env:Body>
</soap-env:Envelope>

の中にある <ns0:image href="cid:spam"/> のような、

  • href 属性を用意
  • href属性に、添付ファイルパートのContent-IDと同じ値を指定

という方式です。

 

wsi:swaRefの実装

wsi:swaRefは、WSDL

<xsd:schema elementFormDefault="qualified" targetNamespace="http://example.com/HelloWorld">
     <xsd:element name="RequestInterface">
         <xsd:complexType>
             <xsd:sequence>
                 <!-- WSI:swaRefのため、swaRef型の引数を用意 -->
                 <xsd:element minOccurs="0" name="image" type="ref:swaRef" />
             </xsd:sequence>
         </xsd:complexType>
     </xsd:element>

のような、

  • WSDLswaRef 型で定義
  • そのelement (上記例だとimage)に、添付ファイルパートのContent-IDと同じ値を指定

という方式です。

 

SwAの実装

SwAは、添付ファイルパートのContent-IDが一意であれば良く、SOAPエンベロープには何も指定しません。

 
ということで、今回は一番簡単そうなSwAについて実装してみます。

なお、今回のSwAで送信するファイルは1つのみです。仕様的には複数添付することも可能なようですので、一部実装を変更すれば動作すると思います。

 

環境

 

WSDLの実装

SwAでは送信するファイルはSOAPエンベロープの外側にあるため、リクエストパラメータは必要としません。

そのため、今回はリクエストパラメータ無しのWSDLを用意します。

swa.wsdl

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions
        xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
        xmlns:soap11="http://schemas.xmlsoap.org/wsdl/soap/"
        xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
        xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:ns0="http://example.com/HelloWorld"
        targetNamespace="http://example.com/HelloWorld">

    <wsdl:types>
        <xsd:schema elementFormDefault="qualified" targetNamespace="http://example.com/HelloWorld">
            <xsd:element name="ResponseInterface">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element minOccurs="0" name="returnMessage" type="xsd:string" />
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
        </xsd:schema>
    </wsdl:types>

    <wsdl:message name="messageIn">
    </wsdl:message>
    <wsdl:message name="messageOut">
        <wsdl:part name="parameters" element="ns0:ResponseInterface" />
    </wsdl:message>

    <wsdl:portType name="SwAPort">
        <wsdl:operation name="requestMessage">
            <wsdl:input message="ns0:messageIn"/>
            <wsdl:output message="ns0:messageOut"/>
        </wsdl:operation>
    </wsdl:portType>

    <wsdl:binding name="SwABindingSoap11" type="ns0:SwAPort">
        <soap11:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <wsdl:operation name="requestMessage">
            <soap11:operation soapAction="http://example.com/HelloWorld/requestMessage" />
            <wsdl:input>
                <soap11:body use="literal"/>
            </wsdl:input>
            <wsdl:output>
                <soap11:body use="literal"/>
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>

    <wsdl:service name="SwAService">
        <wsdl:port name="SwAServicePort" binding="ns0:SwABindingSoap11">
            <soap11:address location="http://localhost:9401/SwABindingSoap11"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

 

Zeepでの実装方法を検討

今回はMTOMのpull requestを参考にして実装してみます。
https://github.com/mvantellingen/python-zeep/pull/314/files

その中では

  • Zeep本体のパッチ
  • Transportの差し替え

にて、MTOMを実装していました。

Zeep本体のパッチは行いたくないため、今回はTransportの差し替えのみで動作するよう、実装してみます。

 
既存のTransportクラスのソースコードを見ると、 post_xml() にコメントが書かれていました。

Post the envelope xml element to the given address with the headers. This method is intended to be overriden if you want to customize the serialization of the xml element. By default the body is formatted and encoded as utf-8. See zeep.wsdl.utils.etree_to_string.

https://github.com/mvantellingen/python-zeep/blob/3.2.0/src/zeep/transports.py#L86

そのため、post_xml()をオーバーライドして実装するのが正しいやり方っぽいです。

 

SwAを使った時のHTTPヘッダ・ボディについて

SwAを使った時のHTTPヘッダ・ボディの公式的な仕様を調べてみましたが、それらしい仕様書が見当たりませんでした*1

そのため、もしSwAが必要になるのであれば、リクエスト/レスポンスの仕様書が渡されると思うので、それを参考に実装するしかないのかなと感じました。

 
そのため、この記事では、SOAP UIで動作した時のHTTPヘッダ・ボディについて書いていきます。

他の環境では動作しないかもしれませんが、気にしないことにします。

 

HTTPヘッダ

HTTPヘッダには、以下のようなエントリがあれば良さそうです。

'http_headers': {
    'SOAPAction': '"http://example.com/HelloWorld/requestMessage"',
    'Content-Type': 'multipart/related; boundary="boundary_ec6c66dd25d44f84b27510babf3f1be0"; type="text/xml"; start="start_e876dcbdba0b4671bc3168ec12685c98"; charset=utf-8',
    'Content-Length': '8056'}

この中で、自分で実装すべきものは

  • Content-Typeヘッダ
  • Content-Lengthヘッダ

の2つです。

詳細は以下の通りです。

ヘッダ 項目 備考
Content-Type メディアタイプ Multipart/Related 1
boundary 任意の値 2
type text/xml
start SOAPメッセージパートの Content-ID の値と同じ値(任意の値)
charset utf-8
Content-Length リクエストボディの長さ

 
備考1. メディアタイプについて

XMLペイロードからバイナリ情報を取り除き、その情報をmultipart/related MIMEコンテンツとして、直接HTTPリクエストに格納すると言うことです。

https://www.ibm.com/developerworks/jp/xml/library/x-tippass/index.html

 
備考2. Content-Typeのboundaryパラメータについて

パート間の区切りを示すための文字列で、 --バウンダリ文字列 を設定します。

また、パート全体が終了する場合は、 --バウンダリ文字列-- を設定します。

参考:MIME(Multipurpose Internet Mail Extensions)~後編:インターネット・プロトコル詳説(4) - @IT

 

HTTPリクエストボディ

shinanogold.png ファイルを

  • ファイル形式は、バイナリ
  • boundaryパラメータは、 boundary_ec6c66dd25d44f84b27510babf3f1be0
  • startパラメータは、 start_e876dcbdba0b4671bc3168ec12685c98

として送信する時のリクエストボディ例は、以下となります。

なお、下記例は、実際のログに

  • iVB...rkJggg== の部分は実際のpngファイルなものの、長いため途中を省略
  • 見やすくするよう、「SOAPメッセージ部分」「送信ファイル部分」のコメントを追加

と手を加えていることに注意します。

<!-- SOAPメッセージ部分 -->
--boundary_ec6c66dd25d44f84b27510babf3f1be0
Content-Type: text/xml; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-ID: start_e876dcbdba0b4671bc3168ec12685c98

<?xml version='1.0' encoding='utf-8'?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body/>
</soap-env:Envelope>
<!-- 送信ファイル部分 -->
--boundary_ec6c66dd25d44f84b27510babf3f1be0
Content-Transfer-Encoding: binary
Content-Type: image/png; name="shinanogold.png"
Content-ID: <ham>
Content-Disposition: attachment; name="shinanogold.png";filename="shinanogold.png"

iVB...rkJggg==
--boundary_ec6c66dd25d44f84b27510babf3f1be0--

 
コード中で設定すべき内容は以下です。

パート 項目
SOAPメッセージ Content-ID HTTPヘッダの start パラメータと合わせる
添付ファイル Content-Transfer-Encoding バイナリの場合 binaryBase64エンコードの場合 base64
Content-Type - メディアタイプ 送信するファイルに合わせる (今回はpngのため、 image/png )
Content-Type - name パラメータ 送信ファイル名
Content-ID 添付ファイルごとに一意になる値、内容は任意
Content-Disposition namefilename パラメータとも、送信ファイル名

 

Transportを実装

既存の zeep.transports.Transport クラスを拡張して、Transportを実装します。

MIMEデータであれば、Pythonの標準モジュール email を使うことで、実装量が減るかもしれません。
19.1. email — 電子メールと MIME 処理のためのパッケージ — Python 3.6.5 ドキュメント

ただ、今回は理解を深めるため、自分で実装していきます。

 

__init__() 部分

元々の __init__() をオーバーライドし、SwAで必要なものを受け取れるようにします。

また、クラス定数 NEW_LINE_FOR_SWA_BODY を bytes型の改行コードとして定義します。

class SwATransport(Transport):
    """ SOAP with Attachment (SwA、非swaRef)用のTransport

    添付ファイルは、binary/Base64エンコードのどちらも可 (引数is_base64izeで切り替え)
    なお、Type Hints は、自作部分のみ記載(Zeepのものは不明/差し替わる可能性があるため)
    """
    NEW_LINE_FOR_SWA_BODY = b'\r\n'

    def __init__(self, file: pathlib.Path, attachment_content_id: str, is_base64ize: bool = False,
                 cache=None, timeout=300, operation_timeout=None, session=None) -> None:
        self.file = file
        self.attachment_content_id = attachment_content_id
        self.is_base64ize = is_base64ize

        # boundaryとSOAP部分のContentIDは任意の値で良いため、内部で生成する
        self.boundary = f'boundary_{self._generate_id_without_hyphen()}'
        self.soap_message_content_id = f'start_{self._generate_id_without_hyphen()}'

        super().__init__(cache=cache, timeout=timeout, operation_timeout=operation_timeout,
                         session=session)

 

改行コードをbytes型で定義した理由

メソッド post_xml() で使う引数 message は、最終的にはrequestsライブラリのSessionクラスにあるメソッド post() の引数 data となります。

 
post()のソースコードを見ると、コメントとして

(optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the :class:Request.

https://github.com/requests/requests/blob/v2.21.0/requests/sessions.py#L574

とありました。

 
Zeepではどの型を渡しているかというと、

# https://github.com/mvantellingen/python-zeep/blob/3.2.0/src/zeep/transports.py#L94
message = etree_to_string(envelope)
return self.post(address, message, headers)

とあり、 etree_to_string() の結果となります。

 
etree_to_string()は、 lxml.etree.tostring() になるようです。

# https://github.com/mvantellingen/python-zeep/blob/3.2.0/src/zeep/wsdl/utils.py#L24
def etree_to_string(node):
    return etree.tostring(
        node, pretty_print=False, xml_declaration=True, encoding='utf-8')

 
lxmlのドキュメントを読むと、

Defaults to ASCII encoding without XML declaration. This behaviour can be configured with the keyword arguments 'encoding' (string) and 'xml_declaration' (bool). Note that changing the encoding to a non UTF-8 compatible encoding will enable a declaration by default.

You can also serialise to a Unicode string without declaration by passing the unicode function as encoding (or str in Py3), or the name 'unicode'. This changes the return value from a byte string to an unencoded unicode string.

https://lxml.de/api/lxml.etree-module.html#tostring

とあり、bytes型が返ってくるようです。

これより、 etree_to_string(envelope) は、SOAPエンベロープXML tree を bytes型へ変換します。よって、requestsにもbytes型で渡すことになります。

requestsには、MIMEメッセージヘッダとSOAPエンベロープを改行コードで連結したものを渡します。SOAPエンベロープがbytes型であることから、改行コードもbytes型にしました。

また、改行コードをbytes型にしたのに合わせ、SwATransportクラスの文字リテラルもbytes型で定義しました。

 
ちなみに、 __init__() では、引数 attachment_content_id はstr型を想定しています。そのため、bytes型リテラルに attachment_content_id を埋め込む時は %演算子 + %a を使います。 *2

 

ユーティリティメソッド

ユーティリティメソッドを2つ用意します。

def _generate_id_without_hyphen(self) -> str:
    """ uuid4()でIDを生成する

    uuid4()だとハイフンも含まれるため、削除しておく
    """
    return str(uuid.uuid4()).replace("-", "")

def _format_bytes(self, target: bytes, values: Iterable[str]) -> bytes:
    """ bytes型にstr型を埋め込む

    埋め込むと、シングルクォート(')まで含まれてしまうため、削除しておく
    """
    return (target % values).replace(b"'", b"")

前者は、任意の値でよかった

  • boundaryパラメータ
  • startパラメータ

向けのID生成メソッドとして使用します。

後者はコメント通りです。今回のリテラルではシングルクォートが存在しなかったため、問題ないと判断しました。

 

残りを実装

あとは、SOAPメッセージ部分を生成するメソッドと

def _create_soap_part(self, envelope) -> bytes:
    """ レスポンスボディのうち、SOAP部分を作成する """
    mime_message_header = self.NEW_LINE_FOR_SWA_BODY.join([
        self._format_bytes(b'--%a', (self.boundary,)),
        b'Content-Type: text/xml; charset=utf-8',
        b'Content-Transfer-Encoding: 8bit',
        self._format_bytes(b'Content-ID: %a', (self.soap_message_content_id,)),
        b'',
    ])

    # etree_to_string()にて、envelopeをbytes型の送信データへ変換する
    return self.NEW_LINE_FOR_SWA_BODY.join([
        mime_message_header,
        etree_to_string(envelope),
    ])

 
SwAで添付するファイル部分を生成するメソッドを用意します。

def _create_attachment_part(self) -> bytes:
    """ レスオンスボディのうち、添付ファイル部分を作成する """
    if not self.file:
        return b''

    transfer_encoding = 'base64' if self.is_base64ize else 'binary'

    # ファイル名からMIMEタイプを推測するが、戻り値はタプルであることに注意
    # Content-Type向けのは1つ目の要素
    # https://docs.python.jp/3/library/mimetypes.html#mimetypes.guess_type
    content_type, _ = mimetypes.guess_type(str(self.file))

    mime_part_header = self.NEW_LINE_FOR_SWA_BODY.join([
        self._format_bytes(b'--%a', (self.boundary,)),
        self._format_bytes(b'Content-Transfer-Encoding: %a', (transfer_encoding,)),
        self._format_bytes(b'Content-Type: %a; name="%a"', (content_type, self.file.name)),
        self._format_bytes(b'Content-ID: <%a>', (self.attachment_content_id,)),
        self._format_bytes(b'Content-Disposition: attachment; name="%a"; filename="%a"',
                           (self.file.name, self.file.name)),
        b'',
    ])

    # 添付ファイルはバイナリ(bytes型)であることに注意
    with self.file.open(mode='rb') as f:
        attachment_data = f.read()

    if transfer_encoding == 'base64':
        attachment_data = base64.b64encode(attachment_data)

    return self.NEW_LINE_FOR_SWA_BODY.join([
        mime_part_header,
        attachment_data,
    ])

 
そして、SOAPメッセージ部分と添付ファイル部分を連結するメソッドを用意します。

def _create_message(self, envelope) -> bytes:
    """ SwA用のリクエストボディを作成する

    requests.post() で送信するため、bytes型で返すこと
    """

    # SOAP部分の作成
    soap_part = self._create_soap_part(envelope)
    # 添付ファイル部分の作成
    attachment_part = self._create_attachment_part()

    return self.NEW_LINE_FOR_SWA_BODY.join([
        soap_part,
        attachment_part,
        self._format_bytes(b'--%a--', (self.boundary,)),
    ])

 
最後に、HTTPリクエストヘッダに追加します。

def _add_swa_header(self, headers: dict, message: bytes):
    """ SwA用のヘッダを追加する """

    headers["Content-Type"] = "; ".join([
        "multipart/related",
        'boundary="{}"'.format(self.boundary),
        'type="text/xml"',
        'start="{}"'.format(self.soap_message_content_id),
        "charset=utf-8"
    ])
    headers["Content-Length"] = str(len(message))

 
上記で各メソッドがそろったため、 post_xml() でそれらを呼び出せば完成です。

def post_xml(self, address, envelope, headers):
    """ オーバーライドして、SwAデータをPOSTできるようにする """

    message = self._create_message(envelope)
    self._add_swa_header(headers, message)

    # SwAの添付ファイル部分はSOAP envelopeの外側にあり、HistoryPluginでは表示されないため、
    # 送信時のmessageを見るためにprint()しておく
    print(message)

    # etree_to_string()はすでに実行済のため、元々のpost_xml()にあった、self.post()だけ実装する
    return self.post(address, message, headers)

 

作成したSwATransportを使用してリクエストを送信

作成した SwATransport クラスは、Zeep Clientを生成する時に使います。

以下は、WSDLに書かれたエンドポイントに requestMessage() を使ってファイルを送信する例です。

# swa_runner.py
def run(attachment_content_id, is_base64ize=False):
    session = Session()
    transport = SwATransport(ATTACHMENT, attachment_content_id=attachment_content_id,
                             is_base64ize=is_base64ize, session=session)

    history_plugin = HistoryPlugin()
    client = Client(str(WSDL), transport=transport, plugins=[history_plugin])
    response = client.service.requestMessage()

    print('--- history ---')
    print('-- header --')
    print(history_plugin.last_sent)
    print('-- SOAP envelope --')
    print(etree.tostring(history_plugin.last_sent['envelope'],
                         pretty_print=True, encoding='unicode'))
    print('--- response ---')
    print(response)


if __name__ == '__main__':
    print('-' * 40)
    print('添付ファイルはバイナリのまま送信')
    print('-' * 40)
    run(attachment_content_id='ham', is_base64ize=False)

    print('-' * 40)
    print('添付ファイルはBASE64化して送信')
    print('-' * 40)
    run(attachment_content_id='spam', is_base64ize=True)

 
ちなみに、Clientを生成した後でも、 transport 属性に設定すれば差し替えられます。

transport = SwATransport(...)
client.transport = transport

 

動作確認

SOAP UIの MockService を使って動作を確認します。

以下の内容を指定して作成後、MockServiceを起動し、 running on port 9401 と表示されればOKです。

項目
initial WSDL 上記で指定した swa.wsdl
Path WSDLで指定したエンドポイント /SwABindingSoap11
Port WSDLで指定したポート 9401

 
また、送信するファイルとして、 shinanogold.png を用意します。

f:id:thinkAmi:20190101165407p:plain:w100

 
次に上記で作成した swa_runner.py を実行します。

実行結果は以下です。見づらかったため、一部を改行しています。

$ python swa_runner.py 
----------------------------------------
添付ファイルはバイナリのまま送信
----------------------------------------
b'--boundary_80e8b8b01b1c47a7870a71622ec74c0b\r\n
Content-Type: text/xml; charset=utf-8\r\n
Content-Transfer-Encoding: 8bit\r\nContent-ID: start_e4e758dda9fc4671a4232b48e4a53360\r\n\r\n

<?xml version=\'1.0\' encoding=\'utf-8\'?>\n
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body/>
</soap-env:Envelope>\r\n
--boundary_80e8b8b01b1c47a7870a71622ec74c0b\r\n
Content-Transfer-Encoding: binary\r\n
Content-Type: image/png; name="shinanogold.png"\r\n
Content-ID: <ham>\r\n
Content-Disposition: attachment; name="shinanogold.png"; filename="shinanogold.png"\r\n\r\n\

x89PNG...\x00IEND\xaeB`\x82\r\n
--boundary_80e8b8b01b1c47a7870a71622ec74c0b--'
--- history ---
-- header --
{'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10ff59cc8>, 
'http_headers': {
  'SOAPAction': '"http://example.com/HelloWorld/requestMessage"', 
  'Content-Type': 'multipart/related; boundary="boundary_80e8b8b01b1c47a7870a71622ec74c0b"; type="text/xml"; start="start_e4e758dda9fc4671a4232b48e4a53360"; charset=utf-8', 
  'Content-Length': '6192'}}
-- SOAP envelope --
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body/>
</soap-env:Envelope>

--- response ---
?
----------------------------------------
添付ファイルはBASE64化して送信
----------------------------------------
b'--boundary_ddb04f0720aa4685ab41072a28818d2a\r\n
Content-Type: text/xml; charset=utf-8\r\n
Content-Transfer-Encoding: 8bit\r\n
Content-ID: start_e5e8c71ed781455a9ed66f214da1a5a7\r\n\r\n

<?xml version=\'1.0\' encoding=\'utf-8\'?>\n
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body/>
</soap-env:Envelope>\r\n
--boundary_ddb04f0720aa4685ab41072a28818d2a\r\n
Content-Transfer-Encoding: base64\r\n
Content-Type: image/png; name="shinanogold.png"\r\n
Content-ID: <spam>\r\n
Content-Disposition: attachment; name="shinanogold.png"; filename="shinanogold.png"\r\n\r\n

iVBOR...AAAABJRU5ErkJggg==\r\n
--boundary_ddb04f0720aa4685ab41072a28818d2a--'
--- history ---
-- header --
{'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x11004d388>, 
'http_headers': {
  'SOAPAction': '"http://example.com/HelloWorld/requestMessage"', 
  'Content-Type': 'multipart/related; boundary="boundary_ddb04f0720aa4685ab41072a28818d2a"; type="text/xml"; start="start_e5e8c71ed781455a9ed66f214da1a5a7"; charset=utf-8',
  'Content-Length': '8057'}}
-- SOAP envelope --
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body/>
</soap-env:Envelope>

--- response ---
?

 
SOAP UIのログを見ると、2つエントリが追加されています。

前者がバイナリ形式での送信、後者がBase64エンコードでの送信のため、それぞれのログを確認します。

 

バイナリ送信の結果

SOAP UIの Headers タブでリクエストヘッダを確認します。

f:id:thinkAmi:20190101165028p:plain:w300

 
Attachments タブで、ファイルが送信されてきたことが確認できました。

また、Type列が MIME となっています。このページの解説より、SOAP UIでもSwAのファイルとして認識されているようです。

f:id:thinkAmi:20190101165127p:plain:w300

 
ファイルを選択し、 Exports the selected attachment to a file でファイルをエクスポートして確認します。

f:id:thinkAmi:20190101165221p:plain:w150

送信したファイルを取得できました。

 

Base64エンコード送信の結果

こちらも、SOAP UI のHeadersタブの結果です。binaryで送信するよりもContent-Lengthが増加しています。

f:id:thinkAmi:20190101165449p:plain:w300

 
Attachmentsタブでも、ファイルが送信されています。Content-IDも、Base64エンコードスクリプトで指定した <spam> になっています。

f:id:thinkAmi:20190101165552p:plain:w300

 
エクスポートした結果も同じです。

f:id:thinkAmi:20190101165543p:plain:w150

 

参考文献

MIMEまわりの定義を知るために読みました。
O'Reilly Japan - 電子メールプロトコル

古い書籍のため、 multipart/related については記載されていませんでしたが、それ以外については参考になりました。

 

ソースコード

GitHubに上げました。 file_attachments/swa 以下が今回のファイルです。
https://github.com/thinkAmi-sandbox/python_zeep-sample

*1:swaRefであれば、W3Cにありました:https://www.w3.org/TR/SOAP-attachments#HTTPBinding

*2:このあたりは以前書きました:Python3で、bytes型の文字列にstr型の文字列を埋め込むため、%演算子 + %a を使う - メモ的な思考的な / http://thinkami.hatenablog.com/entry/2018/12/30/181438