Пакетная обработка почты с Gmail не работает с SMTP, успешно с Google API

У меня есть пакет Node.js, который обрабатывает список пользователей и выполняет вызовы API в серверную часть Java для каждого пользователя. Пакет Node.js и серверная часть Java отправляют письма пользователям с учетной записью G-Suite.

Нет проблем для Node.js, все письма проходят успешно. Что касается Java, после определенного количества отправленных писем следующие письма терпят неудачу со следующим сообщением:

org.springframework.mail.MailAuthenticationException: Authentication
failed; nested exception is javax.mail.AuthenticationFailedException:
334
eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==
;   nested exception is:    javax.mail.AuthenticationFailedException:
OAUTH2 asked for more   at
org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:424)
at
org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:345)
at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:340)

Что после декодирования Base64 дает:

{"status":"400","schemes":"Bearer","scope":"https://mail.google.com/"}

Вот некоторые подробности моих реализаций.

@Service
public class MailServiceImpl implements MailService {

    @Autowired
    private JavaMailSender javaMailSender;

    @Value("${fr.app.email.service-account}")
    private String serviceAccount;

    @Value("${fr.app.email.scope}")
    private String scope;

    private Credential creds;

    SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy à HH:mm");

    @Async
    public CompletableFuture<Void> sendSimpleMail(String email, String subject, String text) throws Exception {

        if (creds == null) {
            final HttpTransport TRANSPORT = new NetHttpTransport();
            final JsonFactory JSON_FACTORY = new JacksonFactory();
            final URL URL = Thread.currentThread().getContextClassLoader().getResource("MyApp-d8a5d9785c54.p12");

            creds = new GoogleCredential.Builder()
                .setTransport(TRANSPORT)
                .setJsonFactory(JSON_FACTORY)
                .setServiceAccountId(serviceAccount)
                .setServiceAccountPrivateKeyFromP12File(new File(URL.getFile()))
                .setServiceAccountScopes(Collections.singleton(scope))
                .setServiceAccountUser(((JavaMailSenderImpl)javaMailSender).getUsername())
                .build();
        }

        if (creds.getExpiresInSeconds() != null && creds.getExpiresInSeconds() < 0 || creds.getAccessToken() == null) {
            creds.refreshToken();
        }

        try {

            ((JavaMailSenderImpl)javaMailSender).setDefaultEncoding("UTF-8");
            ((JavaMailSenderImpl)javaMailSender).setPassword(creds.getAccessToken());
            Message message = new MimeMessage(((JavaMailSenderImpl)javaMailSender).getSession());
            message.setFrom(new InternetAddress(((JavaMailSenderImpl)javaMailSender).getUsername()));
            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email));
            message.setSubject(subject);
            message.setContent(text, "text/html; charset=UTF-8");

            javaMailSender.send((MimeMessage)message);

            return CompletableFuture.completedFuture(null);

        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}

И рабочая реализация Node.js:

const { google } = require('googleapis');

// configure a JWT auth client
let jwtClient = new google.auth.JWT(
    config.appServiceAccount,
    config.googleKeyFile,
    null,
    [config.googleScope],
    config.appMail
);

logger.info('Gmail client created!');

const gmail = google.gmail({ version: "v1", auth: jwtClient });

function sendMail(gmail, jwtClient, to, subject, message) {

    let headers = {
        "To": to,
        "Subject": subject,
        "Content-Type": "text/html; charset=UTF-8"
    }

    let email = '';
    for (var header in headers)
        email += header += ": " + headers[header] + "\r\n";

    email += "\r\n" + message;

    return gmail.users.messages.send({
        auth: jwtClient,
        userId: config.appMail,
        requestBody: {
            raw: btoa(email).replace(/\+/g, '-').replace(///g, '_')
        }
    });
}

У меня есть более подробная информация об ошибке благодаря ключу конфигурации mail.debug=true: первые 80 сообщений дают следующий результат:

DEBUG SMTP: protocolConnect login, host=smtp.gmail.com,
[email protected], password=<non-null> DEBUG SMTP: Attempt to
authenticate using mechanisms: XOAUTH2 DEBUG SMTP: Using mechanism
XOAUTH2 AUTH XOAUTH2 <access_token>
235 2.7.0 Accepted

Следующие сообщения производят вывод:

DEBUG SMTP: protocolConnect login, host=smtp.gmail.com,
[email protected], password=<non-null> DEBUG SMTP: Attempt to
authenticate using mechanisms: XOAUTH2 DEBUG SMTP: Using mechanism
XOAUTH2 AUTH XOAUTH2 <exact same access_token>
334
eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==
DEBUG SMTP: AUTH XOAUTH2 failed, THROW:
javax.mail.AuthenticationFailedException: OAUTH2 asked for more
        at com.sun.mail.smtp.SMTPTransport$OAuth2Authenticator.doAuth(SMTPTransport.java:1095)
        at com.sun.mail.smtp.SMTPTransport$Authenticator.authenticate(SMTPTransport.java:909)
        at com.sun.mail.smtp.SMTPTransport.authenticate(SMTPTransport.java:843)
        at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:748)

Что касается реализация клиента Node.js, основное отличие состоит в том, что Node.js, похоже, не использует SMTP, а вместо этого вызывает API.

Я прочитал здесь, что существуют определенные ограничения для SMTP, но я не могу понять, какое ограничение я достиг, потому что сообщение об ошибке очень нечеткое (DDoS, сообщения за 10 минут, ...).

Вероятно, срок действия вашего токена доступа OAUTH2 истек, и его необходимо обновить.

Bill Shannon 02.11.2018 22:11

@BillShannon моя реализация должна обновлять токен при необходимости. Вы видите что-то не так в моем коде?

Guerric P 04.11.2018 19:55

Убедитесь, что это освежает, когда вы думаете, что это нужно. У вас есть выражение, в котором используются "&&" и "||" и без скобок, действительно ли он делает то, что вы ожидаете?

Bill Shannon 05.11.2018 20:46

@BillShannon Я буду исследовать, но если проблема связана с токеном, вы объясните, почему это происходит только при одновременной отправке большого количества писем? Есть ли какое-то «истечение срока действия bean-компонента», когда он простаивает, что объясняет такое поведение?

Guerric P 05.11.2018 20:55

Срок действия токена зависит от времени. Предположительно, количество отправленных сообщений определяет количество затраченного времени. Но очевидно, что я просто догадываюсь об истечении срока действия токена, вам нужно немного отладить.

Bill Shannon 06.11.2018 00:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
324
0

Другие вопросы по теме