LoginSignup
1
1

DMARCのレポートのメールを Googleスプレッドシートに記録していく(改変)

Last updated at Posted at 2024-03-14

DMARCのレポートのメールを Googleスプレッドシートに記録していく(改変)

0. はじめに

DMARCレポートのメールが、定期的に私のGmailに届く。(ちなみに、GoogleがSPAMと判断する理由がわからない。)これをGASで処理したい、と思って検索すると、Qiita に見つけた。これをありがたくパクらせていただき、docomo, ezweb(KDDI) にも対応するように改変した。ChatGPTを使って。ただ、ChatGPT は「GASで添付の gz ファイルは解凍できないよ。」という。そこで、gz で固められたファイルの解凍もQiitaの記事をありがたくパクらせていただいた。

1. コード

ということで、詳細は上記のページを参照してください!
GoogleスプレッドシートのId を入れれば動くと思います。トリガーで、1日1回動作させると良いでしょう。
※ さらに改変した。(2024-03-21)

  • すでに存在しているReport-IDがある場合にはスプレッドシートに追加しない。重複を許さないことで、1日1回、2日分を処理すれば十分。
  • XMLの要素がない場合にも対応できるようにした。
  • begin と end は人が読める形式にした。ただし、JST。
// オリジナルは 「Gmailに届いたDMARCレポートをGAS(Google Apps Script)でGoogleスプレッドシートに取り込む」
// https://qiita.com/sakamoto_koji/items/d8730110ccc0cf99db42
function myFunction() {
  // スプレッドシート指定
  let sheet = SpreadsheetApp.openById("GoogleスプレッドシートのId").getSheetByName("dmarc_report");

  // 既存のReport-IDを取得する
  const existingIds = sheet.getRange("D2:D" + sheet.getLastRow()).getValues().flat();  // Report-IDがD列に格納されていると仮定

  let xmls = [];  // スプシ書き出し用配列
  let messages;   // メールスレッド
  let blob;       // 添付ファイル(解凍前)
  let fileblob;   // 添付ファイル(解凍後)

  // メール抽出条件指定   Google に限らず、"Report domain" "Report-id"がタイトル文字に含まれていて、 直近2日分
  let query = 'subject: "Report domain" "Report-id" newer_than:2d ';
  let threads = GmailApp.search(query);

  // 取得したスレッドを古いものから新しいものへと並び替える
  threads = threads.reverse();

  for (var i = 0; i < threads.length; i++) {
    messages = threads[i].getMessages();
    console.log("メール件名:" + threads[i].getFirstMessageSubject());

    for (let j = 0; j < messages.length; j++) {
      // ここでメール情報を表示      
      // console.log("Date: " + messages[j].getDate()); // メールの日付
      // console.log("Subject: " + messages[j].getSubject()); // メールの件名
      
      blob = messages[j].getAttachments()[0].copyBlob();
      let fileName = blob.getName();

      try {
        if (fileName.endsWith('.zip')) {               // Google の場合
          fileblob = Utilities.unzip(blob);
        } else if (fileName.endsWith('.xml.gz')) {     // Docomo, Ezweb(KDDI) の場合
          blob.setContentType("application/x-gzip");   // gzip の解凍については、「GASでgzipファイルを解凍する」 https://qiita.com/uca/items/8a5af72eec087b125fd6 参照
          let ungzippedBlob = Utilities.ungzip(blob);  // Corrected method name
          fileblob = [ungzippedBlob];
        } else {
          continue;
        }

        for (let k = 0; k < fileblob.length; k++) {
          xmls = parseXml(fileblob[k].getDataAsString());
          xmls.forEach(item => {
            const itemArray = item.split(",");
            const reportId = itemArray[3]; // Report-IDが配列の4番目にあると仮定

            // Report-IDが既存のものでない場合のみ追加
            if (!existingIds.includes(reportId)) {
              sheet.appendRow(itemArray);
            }
          });
        }
      } catch (e) {
        // Logger.log(e);
        continue;
      }
    }
  }
}

// 要素がなくても動作するようにする。
function safeGetChildText(parentElement, childName) {
  var childElement = parentElement.getChild(childName);
  if (childElement != null) {
    return childElement.getText();
  } else {
    // Logger.log("no child : " + childName);
    return "";  // 子要素が存在しない場合は空文字列を返す
  }
}

// start, end のUNIXタイムスタンプをISO 形式に。
function unixTimestampToJstIsoString(unixTimestamp) {
  // UNIXタイムスタンプをミリ秒単位に変換してDateオブジェクトを作成
  var date = new Date(unixTimestamp * 1000);
  
  // JSTのタイムゾーン('GMT+0900')を指定して、ISO 8601形式で日時をフォーマット
  // 注意: 'Z' はUTCを示すため、JSTであることを明示するには '+09:00' を使用する
  var formattedDate = Utilities.formatDate(date, 'GMT+0900', "yyyy-MM-dd'T'HH:mm");
  
  return formattedDate;
}

// XML データを1行のデータでスプレッドシートに格納する。
function parseXml(fileblob_string) {
  let document = XmlService.parse(fileblob_string);
  let root = document.getRootElement();

  let report_metadata = "";
  let policy_published = "";
  let record_txt = "";
  let rtn_line = [];

  var reportMetadataElement = root.getChild("report_metadata");
  var dateRangeElement = reportMetadataElement.getChild("date_range");
  report_metadata = 
              safeGetChildText(reportMetadataElement, "org_name")
      + "," + safeGetChildText(reportMetadataElement, "email")
      + "," + safeGetChildText(reportMetadataElement, "extra_contact_info")
      + "," + safeGetChildText(reportMetadataElement, "report_id")
      + "," + unixTimestampToJstIsoString(safeGetChildText(dateRangeElement, "begin"))
      + "," + unixTimestampToJstIsoString(safeGetChildText(dateRangeElement, "end"));
      // + "," + safeGetChildText(dateRangeElement, "begin")
      // + "," + safeGetChildText(dateRangeElement, "end");

  var policyPublishedElement = root.getChild("policy_published");
  policy_published = 
      safeGetChildText(policyPublishedElement, "domain")
      + "," + safeGetChildText(policyPublishedElement, "dkim")
      + "," + safeGetChildText(policyPublishedElement, "aspf")
      + "," + safeGetChildText(policyPublishedElement, "p")
      + "," + safeGetChildText(policyPublishedElement, "sp")
      + "," + safeGetChildText(policyPublishedElement, "pct")
      + "," + safeGetChildText(policyPublishedElement, "np");

  let records = root.getChildren("record");
  records.forEach(record => {
    var rowElement = record.getChild("row");
    var policyEvaluatedElement = rowElement.getChild("policy_evaluated");
    var authResultsElement = record.getChild("auth_results");
    var dkimElement = authResultsElement.getChild("dkim");
    var spfElement = authResultsElement.getChild("spf");
    record_txt = 
          safeGetChildText(rowElement, "source_ip")
          + "," + safeGetChildText(rowElement, "count")
          + "," + safeGetChildText(policyEvaluatedElement, "disposition")
          + "," + safeGetChildText(policyEvaluatedElement, "dkim")
          + "," + safeGetChildText(policyEvaluatedElement, "spf")
          + "," + safeGetChildText(record.getChild("identifiers"), "header_from")
          + "," + (dkimElement ? safeGetChildText(dkimElement, "domain")
              + "," + safeGetChildText(dkimElement, "result")
              + "," + safeGetChildText(dkimElement, "selector") : ",,,")
          + "," + (spfElement ? safeGetChildText(spfElement, "domain")
              + "," + safeGetChildText(spfElement, "result") : ",,");
    
    // 1行ずつ配列に入れてく
    rtn_line.push(report_metadata + "," + policy_published + "," + record_txt);
  });  // Fixed: added missing closing parenthesis for forEach loop

  return rtn_line;  // Fixed: corrected return statement placement
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1