ITの隊長のブログ

ITの隊長のブログです。Rubyを使って仕事しています。最近も色々やっているお(^ω^ = ^ω^)

【Play Framework2.x】Mail Pluginを使って、なりすましっぽいメールを送信する(できました)

「なりすまし? ダメにきまってんでしょ!?」

そう却下できたらいいんですが、世の中色々な人や仕事がありますので、そうもいかなかった。

業務改善のためにいわゆる”なりすまし”のメールを、業者向けに送らないといけなかった。

不特定多数に送信するのはNGだが、自分たちのサーバやソリューション向けに送信するのは100歩ゆずっておkすることにした。

Play Frameworkのmail pluginを使っていました。以前このブログで記事書いたことがある。

www.aipacommander.com

MailerAPI mail = init();
mail.setFrom("名字 名前 様 <from@gmail.com>");
mail.setRecipient("to@gmail.com");
mail.setSubject("テストの件名ですよー");
mail.send(views.html.mail.contents.render().toString().trim());

こんな感じ。

しかし。。。

使っているメールサーバが外部のサービスのサーバだっため、登録されているメールアドレスのドメインアドレス以外はエラーするって仕様だったのだ! 隊長ピンチ!

じゃあどうしよ! しかしわたしは元(エセ)ネットワークエンジニアだったのでメールの仕様はこちらのサイトで勉強していたのね。

3 Minutes Networking

メーラーで確認できるtoとfromは基本書き換え可能なため、「headerいじればええんじゃね?」ってひらめいたのだ。

じゃあ実装変えます。

MailerAPI mail = init();
mail.setFrom("from@gmail.com");
mail.addHeader("from", "名字 名前 様 <from@gmail.com>"); // これを追加
mail.setRecipient("to@gmail.com");
mail.setSubject("テストの件名ですよー");
mail.send(views.html.mail.contents.render().toString().trim());

github.com

addHeaderっていうメソッドがあったので、そこでheaderを追加するようにした。

おkおk〜。っと思いきや。

あれあれあれあれ〜? めっちゃエラーがでてる。。。

iandeth.dyndns.org

ぐぐったらこちらの記事が。。。なるほど。マルチバイト悪だな。

じゃあこのコードをJavaで実装するようにしました。

MailerAPI mail = init();
mail.setFrom("from@gmail.com");
mail.addHeader("from", encodeFromString("名字 名前 様", "from@gmail.com>"));
mail.setRecipient("to@gmail.com");
mail.setSubject("テストの件名ですよー");
mail.send(views.html.mail.contents.render().toString().trim());

// ... 省略
private static String encodeFromString(String fromString, String mailAddress) {
    String fromText = "";
    try {
        String encodeFromText = new String(fromString.getBytes("ISO2022JP"));
        String baseEncodeFromText = new String(Base64.encodeBase64(encodeFromText.getBytes()));
        fromText = "=?iso-2022-jp?B?" + baseEncodeFromText + "?=" + " <" + mailAddress + ">";
    } catch (Exception e) {
        Logger.trace(e.toString());
    }
    return fromText;
}

Base64は↓のライブラリをimportした

weblabo.oscasierra.net

やっていることは単純で

  1. ISO2022JPでエンコードして
  2. ↑をBase64エンコードして
  3. エンコードした文字列の先頭に"=?iso-2022-jp?B?"と、末尾に"?="をつける
  4. ↑の文字列に"<メールアドレス>"の文字列を半角空白文字で結合する

これでよし。これでエラーが発生せず、安全(?)でなりすましできるようになりました。

と思いきや。。。。この↑のコードではうまくいかないことがわかりました\(^o^)/オワタ

どうやら、"duplicate from"にひっかかったみたいで、サーバに渡せてもメールサーバがエラーを送信者に返す事態に。

うぐぐぐ・・・!どないせーちゅーねん。

ということで、色々探した結果 Mail Pluginではできないことが発覚しました。タイトル変えます。

んじゃ、どうすればいいの? JavaMailを使いましょう。

import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class Mail
    public static void main(String args[]) {
      try {
          String makeFromText = "テスト様";
          String fromText = encodeFromString(makeFromText, "spoofing@gmail.com"); // なりすましのメール
          sendMail(
            "件名",
            fromText,
            "to@gmail.com",
            "mail body"
          );
      } catch (Exception $e) {
        // ...
      }
    }

    private static void sendMail(String subject, String from, String to, String body) throws Exception {
        // 定数は任意で設定ください
        String contentType = "text/plain; charset=UTF-8";

        Properties props   = new Properties();

        props.put("mail.transport.protocol", "smtp");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");
        props.put("mail.smtp.host", HOST);
        props.put("mail.smtp.port", PORT);
        props.put("mail.smtp.from", MAIL_LOGIN_ID); // これがSMTP Authに使うfrom

        Session mailSession = Session.getInstance(props);
        mailSession.setDebug(true);

        MimeMessage message = new MimeMessage(mailSession);
        message.addFrom(InternetAddress.parse(from)); // mail spoofing
        message.setRecipients(Message.RecipientType.TO, to);
        message.setSubject(subject);
        message.setContent(body, contentType);
        message.setHeader("Content-Transfer-Encoding", "7bit"); // 最初文字化けしたけど、この行を追加したらいけた

        Transport transport = mailSession.getTransport();
        try{
            System.out.println("Sending ....");
            transport.connect(HOST, PORT, FROM_MAILADDRESS, PASSWORD);
            transport.sendMessage(message,message.getRecipients(Message.RecipientType.TO));
            System.out.println("Sending done ...");
        }
        catch(Exception e) {
            System.err.println("Error Sending: ");
            e.printStackTrace();
        }
        transport.close();
    }
}

コメントにも書いていますが、PASSWORDなどの定数は任意で設定ください。

ちなみにGmailの場合は、Session mailSession = Session.getInstance(props);の箇所を下記に拡張する必要がある。

        Session mailSession = Session.getInstance(props,
            new javax.mail.Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(FROM_MAILADDRESS, PASSWORD);
                }
            }
        );

これでうまくいきましたー!やったーー!

ちなみに、最初↑のコードをどっからかパクってきて、動作させようとしましたが、いまいちピンと来ず、混乱していました。

頭を整理するためにも質問サイトで質問事項を書いて、何がわからないのかを質問してみました。

stackoverflow.com

汚い英語だと思いますが、返事が返ってくる! 何故かコメントで。。。(どうアクションすれば良いのだ)。あとみんなやさしいいい。うれしい。

I believe mail.smtp.from is the envelope MAIL FROM

↑は、mail.smtp.fromエンベロープの"MAIL FROM"と言っているのかな? というかメールセッションの中に"from"を設定するところがあるじゃん!

ってことで、コメントと逆の発想でmail.smtp.fromに、メールサーバにログインするときに使うMAIL FROMのIDを記述。

んで、おそらくエンベロープの"from"はmessage.addFrom(InternetAddress.parse(from)); // mail spoofing←ここでしょ。ってことで、ここに記述。

そしたらでキタ━━━━(゚∀゚)━━━━!!

コードはちゃんと読もう&あとでコメントに感謝して解決内容を投稿しておきます。

【Angular2】親子じゃない同じ階層にいるComponentのメソッドを使いたいとき

タイトルだけじゃよくわかりませんね。つまりこういうこと。

  • AppComponent
    • SearchComponent
    • FormComponent
    • ItemComponent
      • LinkComponent

みたいな構造があったときに、Linkでイベントが発生したらItemでとあるメソッドを実行する。とか、ItemからLinkに値を渡すってことはできるようになりました。要は親子関係だったら@Input()とか@Output()を使えばいけるのよね。

が、Formで追加したあとに、ItemのViewを更新したいとなった場合、どうやればいいの?って思ったので、調べたことメモ。

stackoverflow.com

↑が参考になりました。テンプレート上でコンポーネントの変数を用意して、同じ階層にいるコンポーネントに渡せば良いのです。

  • AppComponent.html
<search></search>
<form [itemComponent]="item"></form>
<item #item></item>

↑のように変数の宣言(?)は使う後でもおk。一旦全部解析してからリンクする感じなのかな? なんか多用しちゃうと読みづらいコードになりそう。。。

import { ItemComponent } from './item.component';

@Component({
  selector: 'form',
  templateUrl: './form.component.html'
})

export class FormComponent {
  @Input() private itemComponent: ItemComponent
  // ...
}

これで使えるようになりました。

【CakePHP3.x】Unable to emit headers. Headers sent in file=... line=xxx

今回jsonを返すapiを用意した。前にこのブログでも記事を書いたことがある。

www.aipacommander.com

<?php
...
echo json_encode([]);
return; // returnはいらないけど、強制でactionの処理を終了したいときにあわせて使う

これでいいと思い実装していたのだが、↓のエラーがでてきた。

このエラーと

Unable to emit headers. Headers sent in file=... line=xxx

このエラーである。

Cannot modify header information - headers already sent by ${something php file name}.

発生源はここ

  • ~/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php:42 line
<?php
// ...
    public function emit(ResponseInterface $response, $maxBufferLength = 8192)
    {
        $file = $line = null;
        if (headers_sent($file, $line)) {
            $message = "Unable to emit headers. Headers sent in file=$file line=$line";
            if (Configure::read('debug')) {
                trigger_error($message, E_USER_WARNING);
            } else {
                Log::warn($message);
            }
        }
      // ...
    }
// ...

ふむ?すでにheaderに登録されているとな? よくわからん。

んで、最初jsonの形式がエラーなんだろ?と思っていたけど、どうやらjson_encode()に渡すデータ量が増えるとそのエラーが発生するっぽい。

何故データの量が増えたら、headerに影響するかわからないが、検証したらそうだったので、なんか関係あるのでしょう。(・ω・`)

で、ずっとググっていたが、phpではよく発生しているっぽいけど、cakephpの記事すくなすぎー。

stackoverflow.com

↑の記事のコードをみて、試してみたらビンゴだったので解決策を書く。

responseのオブジェクトにあるbody()へ渡せばよい。

<?php
$this->response->body(json_encode($data));

これでおk。エラーは綺麗サッパリなくなりましたとさ。

【Angular2】Converting circular structure to JSON

FormGroupの値をそのままpostしようとしたら怒られた。

そういえば、jsonの値になっていないじゃないのか。と思い、コンバートしようとする。

stackoverflow.com

どうやら、そんなことはいらないらしい。

onSubmit(value: any): void {
  var url: string = '';
  this.datetimeFormService.postDatetime(url, value.value).subscribe(response => {
    console.log(response);
  });
}

FormGroup.valueでとれる。よかったよかった。