ニッチな仕様のSMTPサーバーを作った件

## こんなメールサーバーが欲しい

普通に使うメールはOutlook+ExchangeOnlineだけど、
自動システムとか内部からのメール送信の一部はオンプレ環境にSMTPサーバーを置き送信を行う必要がある。

必要になったのはこんな機能のあるSMTPサーバー

* ExchangeOnlineでの送信
SMTPで受け取ったメールをExchangeOnlineの非SMTPで送信する。
 SMTPでのログインユーザーでどのアカウントを使うかは決まる
* SMTPリレーとして送信
その場合に、 STARTTLS対応・ DKIM署名付与
* どちらになるかはSMTP認証時のユーザーで決定する

こんなの欲しいが、少し探した限りでは見つからなかった。


## つくる

ExchangeOnlineでの送信に関しては

https://qiita.com/ripple_naip/items/72ffceced10eabefa713

SMTPに関しては
nodemailerはSMTPサーバーが必要なのが基本、
一応direct-transportもあるが、使ってみたらちょっと問題があったので
transporterを作ることにした。
動作作るならとMTA-STSにも対応させた。

nodemailerはdkim署名とSMTP接続の処理に使う。
SMTP接続前の処理は自作。

## 内容

全部コードはのせられないけど一部の処理を載せる、

* メールアドレスからMXサーバーの特定
アドレスのホスト部分のDNSからMXの候補を探し出す。
CNAME,MX,A,AAAAを見て、優先度設定して配列に入れていく

```typescript
getMX = async (domain:string,cnameList?:Array<string>):Promise<undefined|Array<MxRecord>> => {
    if(!domain){
        return;
    }
    const cnames:Array<string> = cnameList||[];
    const lookupLimit = 4;
    //CNAME
    const cnameRR = await this._dnsResolve("CNAME",domain);
    if(cnameRR.record && typeof cnameRR.record[0] === "string"){
        if(cnames.includes(domain)){
            return;
        }
        cnames.push(cnameRR.record[0]);
        if(cnames.length > lookupLimit){
            return;
        }
        const mxList = await this._getMX(cnameRR.record[0],cnames);
        if(!mxList){
            return;
        }
        return [...mxList];
    }
    if(cnameRR.err?.temporary){
        return;
    }
    //MX
    const mxRR = await this._dnsResolve("MX",domain);
    if(mxRR.mxrecord && mxRR.mxrecord.length > 0){
        return [...mxRR.mxrecord];
    }
    if(cnameRR.err?.temporary){
        return;
    }
    //A
    const aRR = await this._dnsResolve("A",domain);
    if(aRR.record){
        return [{"exchange":domain,"priority":0}];
    }
    if(cnameRR.err?.temporary){
        return;
    }
    //AAAA
    const aaaaRR = await this._dnsResolve("AAAA",domain);
    if(aaaaRR.record){
        return [{"exchange":domain,"priority":0}];
    }
    if(cnameRR.err?.temporary){
        return;
    }
}
```

* MXのリスト順に送信を実行する
その際にMXサーバーに対してMTA-STAの検証をする
MTA-STSでTLS必須なら、SMTP接続でTLS検証必須にする。

```typescript
//略
    for(const mx of mxList){
        const mtasts = await this.verifyMTASTS(sendAddressDomain.domain,mx.exchange);
        if(mtasts){
            TLS.DEFAULT_MIN_VERSION = "TLSv1.2";
        }
        const smtpCon = new SMTPConnectionSync({
            "host":mx.exchange,
            "port":25,
            "secure":false,
            "requireTLS":mtasts,
            "tls": {
                "rejectUnauthorized": mtasts,
                "servername":mx.exchange,
            }
        });
        try{
            if(await smtpCon.connect()){
                continue;
            }
            const sendResult = await smtpCon.send({"to":sendAddressDomain.sendAddress,"from":currentQueue.envelopFrom},currentQueue.messege);
            await smtpCon.quit();
            if(sendResult.info && sendResult.info.rejected.length > 0){
                this.pushRetryQueue(currentQueue,{"domain":sendAddressDomain.domain,"sendAddress":sendResult.info.rejected});
            }
            log.debug("Done.");
            //送信成功、MXリスト処理から抜ける
            sendMX = true;
            break;
        }catch(err){
            log.error(err);
            continue;
        }finally{
        //略
        }
    }
    //略
    if(!sendMX){
    //全MXへの処理に失敗、再送キューに入れる
        this.pushRetryQueue(currentQueue,sendAddressDomain);
    }
//略
```


* 送信失敗時の再送処理

これは送信内容をキューに入れて、最初は待機時間=0で即処理、成功したらそのまま終わり、
失敗したら待機時間をセットしてまたキューに入れる。
といった感で処理するようにした。

どうせ、内部でしか使わないし、頻度低いし、で結構適当に作ったけど
それなりに上手く動いている。