Статьи

Отправка почты в Java (и Android) с помощью Apache Commons Net SMTP: STARTTLS, SSL

Недавно работая над экспериментом на Android, я хотел отправлять электронные письма, используя SMTP-сервер, используя аутентификацию и шифрование, из приложения для Android.

Что ж, я обнаружил, что javax.mail на Android не очень хороший вариант , так как он зависит от классов awt (я думаю, это унаследовано); некоторые люди пытались адаптировать его так, чтобы вам не требовался весь пакет awt , но у меня это не получилось ; не говоря уже о тех людях, которые несколько лет назад самостоятельно переделали javax.mail для Android без какого-либо обслуживания.

Еще один вариант, который мне пришёл в голову, — это использование Apache Commons Net : поскольку сообщество добавило SMTPSClient и AuthenticatingSMTPClient к исходному SMTP-клиенту ( и применил небольшой мой патч для SSL и аутентификации ), вы можете встроить эту библиотеку в свой Android приложение (не требуется транзитивных зависимостей) для отправки почты с использованием аутентификации через защищенный уровень. ( этот пост действительно вдохновил меня , но он использует старую версию Apache Commons Net, используя 3.3, вам больше не нужно это делать)

SMTP-аутентификация и STARTTLS с Commons Net

Обычно в этом случае используется порт 25 или альтернативный порт 587. Вы подключаетесь к SMTP-серверу по простому соединению, запрашиваете доступные команды, если поддерживается STARTTLS, вы используете его, а остальная часть связи шифруется.

Давайте рассмотрим пример gmail, поскольку smtp.gmail.com поддерживает аутентификацию и STARTTLS.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public void sendEmail() throws Exception { 
    String hostname = "smtp.gmail.com";
    int port = 587;
  
    String password = "gmailpassword";
    String login = "account@gmail.com";
  
    String from = login;
  
    String subject = "subject" ;
    String text = "message";
  
    AuthenticatingSMTPClient client = new AuthenticatingSMTPClient();
    try {
      String to = "recipient@email.com";
      // optionally set a timeout to have a faster feedback on errors
      client.setDefaultTimeout(10 * 1000);
      // you connect to the SMTP server
      client.connect(hostname, port);
      // you say ehlo  and you specify the host you are connecting from, could be anything
      client.ehlo("localhost");
      // if your host accepts STARTTLS, we're good everything will be encrypted, otherwise we're done here
      if (client.execTLS()) {
  
        client.auth(AuthenticatingSMTPClient.AUTH_METHOD.LOGIN, login, password);
        checkReply(client);
  
        client.setSender(from);
        checkReply(client);
  
        client.addRecipient(to);
        checkReply(client);
  
        Writer writer = client.sendMessageData();
  
        if (writer != null) {
          SimpleSMTPHeader header = new SimpleSMTPHeader(from, to, subject);
          writer.write(header.toString());
          writer.write(text);
          writer.close();
          if(!client.completePendingCommand()) {// failure
            throw new Exception("Failure to send the email "+ client.getReply() + client.getReplyString());
          }
        } else {
          throw new Exception("Failure to send the email "+ client.getReply() + client.getReplyString());
        }
      } else {
        throw new Exception("STARTTLS was not accepted "+ client.getReply() + client.getReplyString());
      }
    } catch (Exception e) {
        throw e;
    } finally {
        client.logout();
        client.disconnect();
    }
  }
  
  private static void checkReply(SMTPClient sc) throws Exception {
    if (SMTPReply.isNegativeTransient(sc.getReplyCode())) {
      throw new Exception("Transient SMTP error " + sc.getReply() + sc.getReplyString());
    } else if (SMTPReply.isNegativePermanent(sc.getReplyCode())) {
      throw new Exception("Permanent SMTP error " + sc.getReply() + sc.getReplyString());
    }

Здесь нечего добавить, конечно, обработка исключений может быть оптимизирована, если вы используете свои собственные классы исключений.

SMTP-аутентификация и SSL с Commons Net

Некоторые SMTP-серверы настроены на прием только «a to z SSL»: вы должны защитить связь прямо перед тем, как отправлять какие-либо команды серверу; обычно используется порт 465.

Давайте возьмем пример LaPoste.net (бесплатные почтовые аккаунты, предлагаемые французской почтой):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public void sendEmail() throws Exception { 
    String hostname = "smtp.laposte.net";
    int port = 465;
  
    String password = "password";
    String login = "firstname.lastname";
  
    String from = login + "@laposte.net";
  
    String subject = "subject" ;
    String text = "message";
  
    // this is the important part : you tell your client to connect using SSL right away
    AuthenticatingSMTPClient client = new AuthenticatingSMTPClient("TLS",true);
    try {
      String to = "anthony.dahanne@gmail.com";
      // optionally set a timeout to have a faster feedback on errors
      client.setDefaultTimeout(10 * 1000);
      client.connect(hostname, port);
      client.ehlo("localhost");
      client.auth(AuthenticatingSMTPClient.AUTH_METHOD.LOGIN, login, password);
      checkReply(client);
  
      client.setSender(from);
      checkReply(client);
  
      client.addRecipient(to);
      checkReply(client);
  
      Writer writer = client.sendMessageData();
  
      if (writer != null) {
        SimpleSMTPHeader header = new SimpleSMTPHeader(from, to, subject);
        writer.write(header.toString());
        writer.write(text);
        writer.close();
        if(!client.completePendingCommand()) {// failure
          throw new Exception("Failure to send the email "+ client.getReply() + client.getReplyString());
        }
      } else {
        throw new Exception("Failure to send the email "+ client.getReply() + client.getReplyString());
      }
    } catch (Exception e) {
        throw e;
    } finally {
        client.logout();
        client.disconnect();
    }

Я не повторял здесь метод checkReply (), так как он одинаков для обоих фрагментов кода; вы заметите, что использование SSL сразу означает, что вам не нужно проверять ответ execTls () (на самом деле он не будет работать, если вы это сделаете).

Завершение

Это об этом; если вы хотите, чтобы эти примеры работали в вашей среде, вы можете добавить apache commons net 3.3 jar в ваш classpath

Если вы используете Maven, добавьте зависимость:

1
2
3
4
5
<dependency>
    <groupid>commons-net</groupid>
    <artifactid>commons-net</artifactid>
    <version>3.3</version>
</dependency>

Если вы используете Gradle для своего проекта Android, вы также можете использовать следующий файл build.gradle:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.4.2'
    }
}
apply plugin: 'android'
  
repositories {
    mavenCentral()
}
  
dependencies {
    compile fileTree(dir: 'libs', include: '*.jar'), 'commons-net:commons-net:3.3'
}
  
android {
    compileSdkVersion 17
    buildToolsVersion "17.0.0"
  
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }
  
        instrumentTest.setRoot('tests')
    }
}

Наслаждайтесь !