読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

SqaleのPHPアプリケーションでSwiftMailerを使ってメール送信してみたメモ

SqaleでPHPアプリケーションを動かす記事の続きです。

最近、メールサーバを導入していない環境でメールを送信するため、Symfonyフレームワークで標準採用されているライブラリ swiftmailer を扱う機会がありました。

Sqale のアプリケーション環境においてもメールサーバが提供されていなかったので、Silexとswiftmailerを使ってフォームからGMailサーバ経由でメール送信してみたメモです。

Sqale - FAQ: 技術的な仕様に関する質問 アプリケーションからメールを送信するための仕組みはありますか?

開発環境にswiftmailerをインストール

開発環境はWindows7で、シェルはmsysGit付属のBashを利用しています。

まずは前回と同様、Composerを使ってswiftmailerをインストール。

composer.json

{
    "require": {
        "silex/silex": "1.0.*@dev",
        "swiftmailer/swiftmailer": "dev-master"
    },
    "config": {
            "vendor-dir": "../vendor"
    }
}

composer update を実行します。ローカルではcomposer.pharはリポジトリのルートにインストール済みです。

$ php composer.phar update
Loading composer repositories with package information
Installing dependencies from lock file
  - Installing swiftmailer/swiftmailer (dev-master b6bfc8f)
    Cloning b6bfc8f7f8ae5dac7883885ee323dc3b53ab7d21
Generating autoload files

これで、swiftmailerのインストールは完了です。

Silexアプリケーションのエントリスクリプト

URLハンドラ部分を省略した全体の構成としては、こんな感じです。

今回はユーザーからのリクエストを扱うことになりますので、Application::protect() で入力値フィルタ用の関数を定義しています。

Application::before() で Request オブジェクトの値を強制的に書き換える(生成し直す)方法や、ParameterBag::filter() を使う方法も考えたのですが、それはそれでどちらも回りくどい感じになりそうだったので、結局こんな単純な対応にしました。

public/index.php より抜粋

<?php
require_once realpath(__DIR__ . '/../../vendor/autoload.php');

use Silex\Application;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;

$app = new Application();

$app['request_filter'] = $app->protect(function($value) {
    if (isset($value) && strlen($value) === 0) {
        $filters = array(
            function($val) {
                // 水平タブ,改行,復帰以外の制御コード(00-08,11,12,14-31,127)を除去
                return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S', '', $val);
            },
            function($val) {
                // 改行コードを統一
                return str_replace("\r", "\n", str_replace("\r\n", "\n", $val));
            },
        );
        foreach ($filters as $filter) {
            $value = (is_array($value)) ? array_map($filter, $value) : $filter($value);
        }
    }
    return $value;
});

// メール送信フォーム
$app->match('/mail', function(Application $app, Request $request) {
    // …中略…
})->method('GET|POST');

// メール送信完了
$app->get('/mail-success', function(Application $app, Request $request) {
    // …中略…
});

$app->run();

メール送信フォームのコードはこんな感じで。

Silex + swiftmailer に絞るという意味で、テンプレートエンジンは使ってません。

public/index.php より抜粋

<?php
// メール送信フォーム
$app->match('/mail', function(Application $app, Request $request) {

    $config = json_decode(file_get_contents(__DIR__ . '/../config/config.json'));

    if (!isset($config->gmail->username)) {
        throw new \RuntimeException('GMailユーザー名が未設定です');
    }
    if (!isset($config->gmail->password)) {
        throw new \RuntimeException('GMailパスワードが未設定です');
    }

    $errors = array();

    if ($request->getMethod() === 'POST') {

        $subject = $app['request_filter']($request->request->get('subject'));
        $body = $app['request_filter']($request->request->get('body'));

        if (!isset($subject) || strlen($subject) === 0) {
            $errors[] = '件名を入力してください';
        }

        if (mb_strlen($subject) > 20) {
            $errors[] = '件名は20文字以内で入力してください';
        }

        if (!isset($body) || strlen($body) === 0) {
            $errors[] = '本文を入力してください';
        }

        if (mb_strlen($body) > 50) {
            $errors[] = '本文は50文字以内で入力してください';
        }

        if (empty($errors)) {
            $transport = \Swift_SmtpTransport::newInstance()
                ->setHost('smtp.googlemail.com')
                ->setPort(465)
                ->setEncryption('ssl')
                ->setUsername($config->gmail->username)
                ->setPassword($config->gmail->password)
            ;
            $mailer = \Swift_Mailer::newInstance($transport);
            $message = \Swift_Message::newInstance()
                ->setSubject($subject)
                ->setFrom(array('k.holy74@gmail.com' => 'k-holy'))
                ->setTo(array('k.holy74@gmail.com' => 'k-holy'))
                ->setBody($body, 'text/plain')
            ;
            $failedRecipients = array();
            if ($mailer->send($message, $failedRecipients)) {
                return new RedirectResponse('/mail-success', 303);
            }
        }
    }

    return new Response(sprintf(<<< CONTENT
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>メール送信フォーム</title>
</head>
<body>
<h1>メールを送信します</h1>
%s
<form method="post" action="/mail" />
<dl>
<dt>件名</dt>
<dd><input type="text" name="subject" value="%s" style="width:20em;" /></dd>
<dt>本文</dt>
<dd><textarea name="body" rows="5" style="width:20em;">%s</textarea></dd>
</dl>
<p><input type="submit" value="送信" /></p>
</form>
</body>
</html>
CONTENT
        , !empty($errors)
            ? sprintf('<ul>%s</ul>', implode("\n", array_map(
                function($var) use ($app) {
                    return sprintf('<li>%s</li>', $app->escape($var));
                },
                $errors)))
            : ''
        , isset($subject) ? $app->escape($subject) : ''
        , isset($body) ? $app->escape($body) : ''
    ));

})->method('GET|POST');

件名と本文を入力するフォームを表示し、POSTメソッドで呼ばれた場合は入力された件名と本文で、GmailSMTPサーバを利用して固定のアドレスにメールを送信するだけの内容です。

Gmailのアカウント情報をコードに含めるわけにもいきませんので、ドキュメントルートの外にconfigディレクトリを作成し、その中の config.json ファイルから設定を読み込みます。

(設定ファイルをリポジトリから除外する方法については後述します)

外部のSMTPサーバを経由する場合、インターネットに繋がっていればローカルでの開発時においてもこのコードでメール送信をテストできます。

ただし、今回のようにSSLを利用するのであれば、OpenSSL関数が有効になっている必要があります。Windowsの場合は php.ini で php_openssl.dll を有効にします。

メール送信完了時にリダイレクトする画面はこんな感じ。普通にHTML返しているだけです。

public/index.php より抜粋

<?php
// メール送信完了
$app->get('/mail-success', function(Application $app, Request $request) {
    return new Response(<<< CONTENT
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>メール送信完了</title>
</head>
<body>
<h1>メールを送信しました</h1>
<p><a href="/mail">戻る</a></p>
</body>
</html>
CONTENT
    );
});

postinstallでリポジトリ外の秘密情報にリンクする

Sqaleではリポジトリのルート直下にpostinstallというファイル名のスクリプトを入れておくことで、デプロイ後にスクリプトを実行させることができます。

今回は Sqale - FAQ: 技術的な仕様に関する質問 パスワードのような公開したくない情報を簡単に扱う仕組みはありますか? を参考に、あらかじめリポジトリの外に設置しておいたGmailのアカウント情報を記述した設定ファイルに対し、postinstallでリンクを張ることにします。

#!/bin/sh

set -e
cd $HOME
ln -s /home/sqale/etc/config.json /home/sqale/current/config/config.json

このように設定した上で、アプリケーションのデプロイを行う前に、サーバ上に /home/sqale/etc/config.json を作成しておきます。

{
    "gmail": {
        "username": "GMailアカウント",
        "password": "GMailパスワード"
    }
}

ローカルでの開発時はリンクではなく直接このファイルを設置しておきますが、デプロイ対象からは除外するため、.gitignore に config/config.json を追加しておきます。

前述のメール送信フォームのコードでは "$config = json_decode(file_get_contents(DIR . '/../config/config.json'));" と公開ディレクトリからの相対指定にしていますので、ローカルではファイルから直接、サーバではシンボリックリンク経由で読み込まれるようになります。

また、configディレクトリには空のディレクトリのみをリポジトリに含めるため .gitkeep を置いておきます。

他の開発時のみ利用するファイルも併せて .gitignore の定義は以下のようになりました。

composer.phar
*.lnk
public/router.php
config/config.json

ちなみに "*.lnk" は開発時に利用するビルトインサーバの起動用ショートカット、"public/router.php" はビルトインサーバ用ルータースクリプトです。

ローカルの状態はこんな感じです。*付きがGitリポジトリの管理対象です。

vendor/
demo/
   *.gitignore
    builtin-server.lnk
   *composer.json
   *composer.lock
    composer.phar
   *postinstall
   *config/
       *.gitkeep
        config.json
   *public/
       *.htaccess
       *.undeletable
       *favicon.ico
       *index.php
        router.php

この状態で git push して SSH から確認したところ、シンボリックリンクが作成されていることを確認できました。

$ ls /home/sqale/current/config -l
合計 0
lrwxrwxrwx 1 sqale sqale 27  3月 25 18:45 2013 config.json -> /home/sqale/etc/config.json

postinstallでcomposerコマンドを実行する

せっかくpostinstallスクリプトでデプロイ時のコマンド実行ができるので、そこからの composer update にも挑戦してみました。

(先ほどの postinstall に1行追加しています)

#!/bin/sh

set -e
cd $HOME
ln -s /home/sqale/etc/config.json /home/sqale/current/config/config.json

/home/sqale/.phpenv/shims/php /home/sqale/bin/composer.phar update -d /home/sqale/current/

初めは "php composer.phar update -d /home/sqale/current/" というように、SSHでログインした場合のコマンドと同じものを記述してみましたが、デプロイには成功するもののコマンドは反映されませんでした。

ふと "which php" してみたところ "~/.phpenv/shims/php" と表示されましたので、これをフルパスで指定したところ実行できました。

PHPer的には、デプロイ用のPHPスクリプトを別途記述しておいて、postinstallにはそれを実行するよう指定するだけの方が楽かもしれません。

デプロイ時に静的コンテンツを生成したりと、色々な可能性がありそうです。