You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Gmail API与ASP.NET Core Web API发邮件的本地测试问题咨询

Gmail API邮件发送POC问题解答

背景

基于ASP.NET Core Web API + Google.Apis.Gmail.v1开发邮件发送POC,因2024年9月30日后SMTP方式将被停用,转而使用Gmail API。已在Google开发者控制台创建Web应用OAuth客户端ID并启用Gmail API,本地测试时发现错误信息中的http://127.0.0.1:xxxx端口每次启动都会变化,引发OAuth重定向不匹配错误。


问题解答

1. 端口变化是否由本地测试导致?

是的,这完全是本地调试的特性。ASP.NET Core默认使用随机端口启动本地服务(除非手动配置固定端口),因此每次启动应用,http://127.0.0.1后的端口都会变化。而你在Google开发者控制台配置的OAuth重定向URI是固定值,两者不匹配就会触发错误。

2. 部署到Azure App Service能否解决该错误?

可以解决,但需要完成两个步骤:

  • 部署后,Azure App Service会提供固定的应用域名/URL,把这个URL添加到Google开发者控制台的授权重定向URI列表中
  • 首次部署后需要触发OAuth授权流程(访问API触发发送邮件逻辑),完成授权后令牌会存入你的数据库,后续即可正常发送邮件

3. 可行的本地测试方法

方法一:配置ASP.NET Core使用固定端口

在项目的launchSettings.json中设置固定端口:

"profiles": {
  "http": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://127.0.0.1:5000", // 指定固定端口
    "environmentVariables": {
      "ASPNETCORE_ENVIRONMENT": "Development"
    }
  }
}

然后将http://127.0.0.1:5000添加到Google开发者控制台的重定向URI列表中。

方法二:指定OAuth回调固定端口

修改GoogleWebAuthorizationBroker.AuthorizeAsync的调用,自定义本地回调端口:

var newCredential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
    secrets,
    Scopes,
    "user",
    CancellationToken.None,
    new NullDataStore(),
    new LocalServerCodeReceiver(8080) // 指定固定端口8080
);

同时在Google控制台中添加http://localhost:8080作为重定向URI。

方法三:复用已有令牌跳过OAuth流程

如果已经通过线上环境或其他方式获取了有效的Gmail API访问令牌,直接将令牌存入本地数据库,测试时代码会直接读取令牌,跳过OAuth授权流程,避免端口不匹配问题。注意:令牌过期后需要重新获取。


你的代码

using GeoConnect.Clients;
using GeoConnect.EmailPOC;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Threading;
using Volo.Abp.Application.Services;

namespace GeoConnect.EmailSendingPOC;

public class EmailSendingPocService : ApplicationService
{
    private readonly string[] Scopes = { GmailService.Scope.GmailSend };
    private readonly string ApplicationName = "Gmail API .NET Example";
    private readonly string ClientId = "";
    private readonly string ClientSecret = "";
    private readonly IRepository<EmailSendingPoc, Guid> _tokenRepository;

    public EmailSendingPocService(IRepository<EmailSendingPoc, Guid> tokenRepository)
    {
        _tokenRepository = tokenRepository;
    }

    public async Task SendEmail(string to="test.vishnukumarps@gmail.com",string from= "test.vishnukumarps@gmail.com")
    {
        try
        {
            var credential = await GetUserCredential();

            // Create Gmail API service
            var service = new GmailService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });

            var email = CreateEmail(to,from , "Subject", "Hello, this is a test email!");
            var message = new Message
            {
                Raw = Base64UrlEncode(email)
            };

            // Send the email
            await service.Users.Messages.Send(message, "me").ExecuteAsync();

        }
        catch (Exception ex)
        {
            throw;
        }
    }

    private async Task<UserCredential> GetUserCredential()
    {
        var secrets = new ClientSecrets
        {
            ClientId = ClientId,
            ClientSecret = ClientSecret
        };

        var token = await _tokenRepository.FirstOrDefaultAsync();

        if (!string.IsNullOrEmpty(token?.Token))
        {
            // Create a UserCredential object using the stored token
            var credential = new UserCredential(new GoogleAuthorizationCodeFlow(
                new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = secrets,
                    Scopes = Scopes
                }),
                "user",
                new TokenResponse { AccessToken = token?.Token }
            );
            return credential;
        }

        var newCredential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
            secrets,
            Scopes,
            "user",
            CancellationToken.None,
            new NullDataStore() // No data store in this case
        );

        // Save the token to the database
        // await _tokenRepository.SaveTokenAsync("user", newCredential.Token.AccessToken, newCredential.Token.Expiration);
        await _tokenRepository.InsertAsync(new EmailSendingPoc()
        {
            Token = newCredential.Token.AccessToken
        }, true);

        return newCredential;
    }

    private string CreateEmail(string to, string from, string subject, string body)
    {
        var message = new StringBuilder();
        message.AppendLine($"To: {to}");
        message.AppendLine($"From: {from}");
        message.AppendLine($"Subject: {subject}");
        message.AppendLine("Content-Type: text/plain; charset=utf-8");
        message.AppendLine();
        message.AppendLine(body);

        return message.ToString();
    }

    private static string Base64UrlEncode(string input)
    {
        var bytes = Encoding.UTF8.GetBytes(input);
        return Convert.ToBase64String(bytes)
            .Replace('+', '-')
            .Replace('/', '_')
            .Replace("=", "");
    }
}

内容的提问来源于stack exchange,提问作者VishnuKumar ps

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 13:09:52