Spring Boot+Apache Velocity+AWS Simple Email Serviceでメール送信を実装する


Webサービスの「新規登録」の際に、アカウントの有効化のためのemailを送信する機能を実装する必要があります。

詳しく書くと、

  • ユーザー名、パスワード、メールアドレスを登録するアカウント新規登録フォームを作成
  • アカウントが新規作成されたら、認証用のリンクアドレスを付与したメールを登録されたメールアドレスに対して送信
  • 認証用のリンクアドレスがクリックされたら、アカウントを有効化し、そのアカウントでWebサービスが使えるようになる

といった仕様ですね。

そこで、

  • サーバー側の基本フレームワークとしてのSpring Boot
  • Amazon Web Serviceでメールを送信できるAWS Simple Email Service(SES)
  • メールのテンプレートとして使用できるApache Velocity

を組み合わせて、認証メールを送信するような処理を実装してみました。

メールを送信する用途で使用するため、汎用的なVelocityを使いましたが、実はthymeleafとかでもいけるかもしれません。

ちなみにWebのテンプレートエンジンとしてVelocityを使うのはSpring Bootではすでに非推奨となっています。

6年間ほどVelocity公式の更新が止まっているからですね。。。(多分、HTMLテンプレートとしてはthymeleafを使うのがオススメです。)

それでは、やってみましょう。

※AWSのアカウントを取得していることが前提です。

AWS SESの設定

公式ドキュメントを参考に、アクセスキー・トークンの取得と送信元メールアドレスのverificationを行っておきます。

AWS SESは2016年11月時点ではコンソール・リージョンともにJapanは対応していませんが、リージョンはオレゴンを選択し、コンソールが英語であることを受け入れれば使用をすることは可能です。

取得したアクセスキー・アクセストークン、認証済メールアドレスはsrc/main/resources内にaws-ses.propertiesファイルを作って格納しておきましょう。

aws.ses.accessKey=thisisaccesskey
aws.ses.accessSecret=thisisaccesssecret
aws.ses.fromAddress=hoge.fuga@gmail.com

後ほどBean Configurationでこのプロパティファイルを指定したClassを書くので、プロパティファイルのファイル名やキー名はこの名前に従わなくても、指定先と合致していればOKです

build.gradleにapache velocityとAWS ses SDKを追加

build.gradleに依存関係の記述を追加して、apache velocityとAWS sdkを使えるようにします。


dependencies {
  //~~
  // https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-ses
  compile group: 'com.amazonaws', name: 'aws-java-sdk-ses', version: '1.11.49'
  // https://mvnrepository.com/artifact/org.apache.velocity/velocity
  compile group: 'org.apache.velocity', name: 'velocity', version: '1.7'
}

Maven使っている場合は、pom.xmlを追記しましょう。(今回の記事ではpom.xmlに関する記述は省略します)

Maven Repositoryから探したいライブラリ名で検索をかけると、build.gradleを使用している場合でも、pom.xmlを使用している場合でも適切な記述を見つけることができます。

VelocityContextをSpring Beanに追加

Springの@AutowiredでVelocity Contextオブジェクトを取得できるように、BeanConfigurationを記述します。

/**
 * メールのテンプレートエンジンとして使用するVelocityのBeanConfiguration
 */
@Configuration
@PropertySource(value = "classpath:velocity.properties")
public class VelocityConfig {

  @Value("${resource.loader}")
  private String resourceLoader;

  @Value("${class.resource.loader.class}")
  private String classResourceLoaderClass;

  @Value("${input.encoding}")
  private String inputEncoding;

  @Value("${output.encoding}")
  private String outputEncoding;

  @Bean
  public VelocityContext velocityContext() {
    Properties p = new Properties();
    p.setProperty("resource.loader", resourceLoader);
    p.setProperty("class.resource.loader.class", classResourceLoaderClass);

    p.setProperty("input.encoding", inputEncoding);
    p.setProperty("output.encoding", outputEncoding);

    Velocity.init(p);
    return new VelocityContext();
  }
}

src/main/resources以下にvelocity.propertiesを記述します。

resource.loader=class
class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
input.encoding=UTF-8
output.encoding=UTF-8

velocityを使って送信したいメールの本文を書く

src/main/resources/velocity/activate-mail.vmを作成し、送信したいメールの本文を記述します。

今回はシンプルに、以下のような記述にします。

本サービスにご登録いただき、ありがとうございます。

以下のURLよりアカウントの有効化を行ってください。

${activateUrl}

${activateUrl}に、アカウントの有効化リンクが貼られる設計です、

AWS SESのConfigrationをSpring Beanに追加

AWS SESのServiceクラスを@Autowiredでインジェクションできるように、AWS SESのBeanConfigurationクラスを作成します。

/**
 * AWSのSES(メール送信サービス)についての設定を管理するBean
 */
@Configuration
@PropertySource(value = "classpath:aws-ses.properties")
public class AwsSESConfig {

   //@PropertySourceアノテーションで指定したプロパティファイルのキーの値を格納
  @Value("${aws.ses.accessKey}")
  private String accessKey;

  @Value("${aws.ses.accessSecret}")
  private String accessSecret;

  @Value("${aws.ses.fromAddress}")
  private String from;

  @Bean
  public AmazonSimpleEmailService amazonSimpleEmailService() {
    AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret);
    ClientConfiguration configuration = new ClientConfiguration();
    configuration.setConnectionTimeout(3000);
    AmazonSimpleEmailService client = new AmazonSimpleEmailServiceClient(credentials);
    client.setRegion(Region.getRegion(Regions.US_WEST_2));
    return client;
  }

  @Bean
  public AwsSesFromEmail awsSesFromEmail() {
    AwsSesFromEmail awsSesFromEmail = new AwsSesFromEmail(from);
    return awsSesFromEmail;
  }
}

送信元メールアドレスもBeanComponentとして取得できるようにしています。

@Data
@AllArgsConstructor
public class AwsSesFromEmail {

  private String from;
}

このあたりはうまくやればもう少し記述量が減らせるかもしれません。

アカウントを作成、認証メールを飛ばすビジネスロジックを記述したServiceクラスを作成

@Service
public class AccountCreateService {
  @Autowired
  private AppUserRepository repository;

  @Autowired
  private PasswordEncoder passwordEncoder;

  @Autowired
  private AwsSESConfig sesConfig;

  @Autowired
  private AwsSesFromEmail awsSesFromEmail;

  @Autowired
  private VelocityContext ctx;


  public void create(String username, String rawPassword, String email) {
    String activationKey = DigestUtils.sha256Hex(String.valueOf(Calendar.getInstance().getTimeInMillis()));
    AppUser appUser = createNewUser(username, rawPassword, email, activationKey);

    repository.save(appUser);

    //SESでメール送信
    Destination destination = new Destination().withToAddresses(email);
    Content subject = new Content().withData("新規登録ありがとうございます");
    Content body = new Content().withData(buildEmailBody(activationKey));

    Message message = new Message().withSubject(subject).withBody(new Body().withText(body));
    SendEmailRequest request = new SendEmailRequest().withSource(awsSesFromEmail.getFrom())
        .withDestination(destination).withMessage(message);
    sesConfig.amazonSimpleEmailService().sendEmail(request);
  }

  public boolean activate(String activationKey) {
    List<AppUser> users = repository.findByActivationKey(activationKey);
    if (users.isEmpty()) {
      return false;
    }
    AppUser user = users.get(0);
    if (user.isActivated()) {
      return false;
    }
    user.setActivated(true);
    user.setLastLoginAt(new Timestamp(Calendar.getInstance().getTimeInMillis()));
    repository.save(user);
    return true;
  }

  private AppUser createNewUser(String username, String rawPassword, String email, String activationKey) {
    AppUser appUser = new AppUser();
    appUser.setUsername(username);
    appUser.setActivated(false);
    appUser.setEmail(email);
    appUser.setEncodedPassword(passwordEncoder.encode(rawPassword));

    Timestamp now = new Timestamp(Calendar.getInstance().getTimeInMillis());
    appUser.setCreatedAt(now);
    appUser.setLastLoginAt(now);

    appUser.setActivationKey(activationKey);
    return appUser;
  }

  private String buildEmailBody(String activationKey) {
    String url = "http://localhost:8086/account/activate?p=" + activationKey;
    ctx.put("activateUrl", url);

    Template template = Velocity.getTemplate("velocity/activate-mail.vm", "UTF-8");

    StringWriter sw = new StringWriter();
    template.merge(ctx, sw);
    sw.flush();
    return sw.toString();
  }
}

実際にVelocity周りの操作を行っているのはbuildEmailBodyですね。

vmファイルと変数のバインドを行い、バインドされた後の文章を文字列として返しています。

アカウント作成画面のControllerとViewを記述

Controller

@RequestMapping("/account")
@Controller
public class AccountCreateController {

  @Autowired
  private AccountCreateService accountCreateService;


  @RequestMapping("/new")
  public String newAccount() {
    return "account/new-account";
  }

  @RequestMapping(value = "/create", method = RequestMethod.POST)
  public String create(AccountCreateForm form, BindingResult result) {
    if (result.hasErrors()) {
      return "account/new-account";
    }
    accountCreateService.create(form.getUsername(),form.getPassword(),form.getEmail());
    return "account/created";
  }

  @RequestMapping(value = "/activate", method = RequestMethod.GET)
  public String activate(@RequestParam("p") String activationKey, Model model) {
    return accountCreateService.activate(activationKey) ? "account/activate-success":"account/activate-failure";
  }
}

thymeleafはこんな感じになります。シンプルな送信フォームですね。

account/new-account.html

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
<p>アカウントの新規作成</p>

<form action="/account/create" method="post">

    <input type="text" name="username" placeholder="username"/>
    <input type="password" name="password" placeholder="password"/>
    <input type="email" name="email" placeholder="email"/>

    <button type="submit">作成</button>

</form>

</body>
</html>

先のControllerで指定している、activate-success.htmlなどは「成功しました」「失敗しました」しか書いてないので省略します。

まとめ

以上のような手順で、Spring+Velocity+AWSSESでメールを送信することができます。

感想としては、メール一本発行するのに結構な手間だな、という感じですが。。。