All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.ext.mail.impl.MailClientImpl Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright (c) 2011-2015 The original author or authors
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *       The Eclipse Public License is available at
 *       http://www.eclipse.org/legal/epl-v10.html
 *
 *       The Apache License v2.0 is available at
 *       http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.mail.impl;

import io.vertx.core.*;
import io.vertx.core.internal.ContextInternal;
import io.vertx.core.internal.logging.Logger;
import io.vertx.core.internal.logging.LoggerFactory;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.core.shareddata.Shareable;
import io.vertx.ext.mail.MailClient;
import io.vertx.ext.mail.MailConfig;
import io.vertx.ext.mail.MailMessage;
import io.vertx.ext.mail.MailResult;
import io.vertx.ext.mail.impl.dkim.DKIMSigner;
import io.vertx.ext.mail.mailencoder.EncodedPart;
import io.vertx.ext.mail.mailencoder.MailEncoder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * MailClient implementation for sending mails inside the local JVM
 *
 * @author Alexander Lehmann
 */
public class MailClientImpl implements MailClient {

  private static final Logger log = LoggerFactory.getLogger(MailClientImpl.class);

  private static final String POOL_LOCAL_MAP_NAME = "__vertx.MailClient.pools";

  private final Vertx vertx;
  private final MailConfig config;
  private final SMTPConnectionPool connectionPool;
  private final MailHolder holder;
  // hostname will cache getOwnhostname/getHostname result, we have to resolve only once
  // this cannot be done in the constructor since it is async, so its not final
  private volatile String hostname = null;

  private volatile boolean closed = false;

  // DKIMSigners may be initialized in the constructor to reuse on each send.
  // the constructor may throw IllegalStateException because of wrong DKIM configuration.
  private final List dkimSigners;

  public MailClientImpl(Vertx vertx, MailConfig config, String poolName) {
    this.vertx = vertx;
    this.config = config;
    this.holder = lookupHolder(poolName, config);
    this.connectionPool = holder.pool();
    if (config != null && config.isEnableDKIM() && config.getDKIMSignOptions() != null) {
      dkimSigners = config.getDKIMSignOptions().stream().map(ops -> new DKIMSigner(ops, vertx)).collect(Collectors.toList());
    } else {
      dkimSigners = Collections.emptyList();
    }
  }

  @Override
  public Future close() {
    if (closed) {
      throw new IllegalStateException("Already closed");
    }
    closed = true;
    return holder.close();
  }

  @Override
  public Future sendMail(MailMessage email) {
    ContextInternal context = (ContextInternal) vertx.getOrCreateContext();
    Promise promise = context.promise();
    if (!closed) {
      validateHeaders(email, context)
        .flatMap(ignored -> getHostname())
        .flatMap(ignored -> getConnection(promise::fail, context))
        .flatMap(conn -> sendMessage(email, conn, context).compose(
          result -> conn.returnToPool().transform(ignored -> context.succeededFuture(result)),
          failure -> conn.quitCloseConnection().transform(ignored -> context.failedFuture(failure))))
        .onComplete(promise);
    } else {
      promise.fail("mail client has been closed");
    }
    return promise.future();
  }

  private Future validateHeaders(MailMessage email, ContextInternal context) {
    if (email.getBounceAddress() == null && email.getFrom() == null) {
      return context.failedFuture("sender address is not present");
    } else if ((email.getTo() == null || email.getTo().size() == 0)
      && (email.getCc() == null || email.getCc().size() == 0)
      && (email.getBcc() == null || email.getBcc().size() == 0)) {
      log.warn("no recipient addresses are present");
      return context.failedFuture("no recipient addresses are present");
    } else {
      return context.succeededFuture();
    }
  }

  private Future getHostname() {
    if (hostname != null) {
      return Future.succeededFuture();
    }

    return vertx.executeBlocking(() -> {
      if (config.getOwnHostname() != null) {
        return config.getOwnHostname();
      } else {
        return Utils.getHostname();
      }
    }).andThen(res -> {
      if (res.succeeded()) {
        hostname = res.result();
      }
    });
  }

  private Future getConnection(Handler errorHandler, ContextInternal context) {
    return connectionPool.getConnection(hostname, context)
      .map(conn -> {
        conn.setExceptionHandler(errorHandler);
        return conn;
      });
  }

  private Future dkimSign(ContextInternal context, EncodedPart encodedPart) {
    if (dkimSigners.isEmpty()) {
      return context.succeededFuture();
    }

    List> dkimFutures = new ArrayList<>();
    // run dkim sign, and add email header after that.
    dkimSigners.forEach(dkim -> dkimFutures.add(dkim.signEmail(context, encodedPart)));
    return Future.all(dkimFutures).map(f -> {
      List dkimHeaders = dkimFutures.stream().map(fr -> fr.result().toString()).collect(Collectors.toList());
      encodedPart.headers().add(DKIMSigner.DKIM_SIGNATURE_HEADER, dkimHeaders);
      return null;
    });
  }

  private Future sendMessage(MailMessage email, SMTPConnection conn, ContextInternal context) {
    try {
      final MailEncoder encoder = new MailEncoder(email, hostname, config);
      final EncodedPart encodedPart = encoder.encodeMail(); // may throw
      final String messageId = encoder.getMessageID();
      final SMTPSendMail sendMail = new SMTPSendMail(context, conn, email, config, encodedPart, messageId);

      return dkimSign(context, encodedPart)
        .flatMap(ignored -> sendMail.startMailTransaction());
    } catch (Exception e) {
      return context.failedFuture(e);
    }
  }

  public SMTPConnectionPool getConnectionPool() {
    return connectionPool;
  }

  private MailHolder lookupHolder(String poolName, MailConfig config) {
    synchronized (vertx) {
      LocalMap map = vertx.sharedData().getLocalMap(POOL_LOCAL_MAP_NAME);
      MailHolder theHolder = map.get(poolName);
      if (theHolder == null) {
        theHolder = new MailHolder(vertx, config, () -> removeFromMap(map, poolName));
        map.put(poolName, theHolder);
      } else {
        theHolder.incRefCount();
      }
      return theHolder;
    }
  }

  private void removeFromMap(LocalMap map, String dataSourceName) {
    synchronized (vertx) {
      map.remove(dataSourceName);
      if (map.isEmpty()) {
        map.close();
      }
    }
  }

  private static class MailHolder implements Shareable {
    final SMTPConnectionPool pool;
    final Runnable closeRunner;
    int refCount = 1;

    MailHolder(Vertx vertx, MailConfig config, Runnable closeRunner) {
      this.closeRunner = closeRunner;
      this.pool = new SMTPConnectionPool(vertx, config);
    }

    SMTPConnectionPool pool() {
      return pool;
    }

    synchronized void incRefCount() {
      refCount++;
    }

    synchronized Future close() {
      if (--refCount == 0) {
        Future result = pool.doClose();
        if (closeRunner != null) {
          closeRunner.run();
        }
        return result;
      } else {
        return Future.succeededFuture();
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy