diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailQuery.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailQuery.cs new file mode 100644 index 0000000..1c5c943 --- /dev/null +++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailQuery.cs @@ -0,0 +1,125 @@ +#if NET +using AutoMapper; +using DigitalData.MessagingService.Application.Common.Dto; +using DigitalData.MessagingService.Application.Common.Dto.MailSearch; +using DigitalData.MessagingService.Application.Common.Interfaces; +using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; +using DigitalData.MessagingService.Application.EmailAccounts.Queries; +using DigitalData.MessagingService.Domain.Entities; +using DigitalData.MessagingService.Domain.Enums; +using DigitalData.MessagingService.Domain.Exceptions; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DigitalData.MessagingService.Application.EmailReceiving.Queries; + +/// +/// Query to fetch emails from an email account. +/// The protocol and authentication method are determined by . +/// +public record ReadEmailQuery : IRequest +{ + /// + /// Identifies the email account to use. + /// + public required GetEmailAccountQuery Account { get; init; } + + /// + /// Mail query used to filter and limit the emails retrieved. + /// + public MailSearchFilter Mail { get; init; } = new(); +} + +public class ReadEmailQueryHandler( + IMapper Mapper, + ILogger Logger, + IRepository EmailAccountRepo, + IReceivedEmailRepository MailRepo, + IImapEmailService ImapEmailService, + IPop3EmailService Pop3EmailService) : IRequestHandler +{ + public async Task Handle(ReadEmailQuery request, CancellationToken cancellationToken) + { + var accounts = await EmailAccountRepo.FindAsync( + request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username, + cancellationToken: cancellationToken); + + if (accounts.Count() > 1) + Logger.LogWarning("Multiple email accounts found for the given criteria ({Criteria}). Returning the first one.", + request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}"); + + var account = accounts.FirstOrDefault() + ?? throw new NotFoundException($"No email account found for the given criteria (Id: {request.Account.Id}, Username: {request.Account.Username})."); + + return account.IncomingProtocol switch + { + IncomingProtocol.Imap => await HandleImapAsync(account, request.Mail, cancellationToken), + IncomingProtocol.ImapOAuth2 => await HandleImapOAuth2Async(account, request.Mail, cancellationToken), + IncomingProtocol.Pop3 => await HandlePop3Async(account, request.Mail, cancellationToken), + IncomingProtocol.Pop3OAuth2 => throw new NotImplementedException( + $"POP3 OAuth2 is not yet supported. Account: '{account.Username}' (Id: {account.Id})."), + IncomingProtocol.None => throw new BadRequestException( + $"Account '{account.Username}' (Id: {account.Id}) has no incoming protocol configured (IncomingProtocol = None)."), + _ => throw new BadRequestException( + $"Unknown IncomingProtocol value '{account.IncomingProtocol}' for account '{account.Username}' (Id: {account.Id}).") + }; + } + + private async Task HandleImapAsync(EmailAccount account, MailSearchFilter filter, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(account.ImapServer)) + throw new BadRequestException( + $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration."); + + var mails = await MailRepo.FindAsync(filter, account, cancellationToken); + var lastSync = ImapEmailService.GetLastImapSyncDate(account.Id, filter.Folder); + + return new ReadEmailQueryResponse + { + LastSync = lastSync, + Emails = Mapper.Map>(mails) + }; + } + + private async Task HandleImapOAuth2Async(EmailAccount account, MailSearchFilter filter, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(account.OAuth2ClientId) || string.IsNullOrWhiteSpace(account.OAuth2ClientSecret)) + throw new BadRequestException( + $"OAuth2 credentials (ClientId, ClientSecret) are not configured for account '{account.Username}' (Id: {account.Id})."); + + if (string.IsNullOrWhiteSpace(account.ImapServer)) + throw new BadRequestException( + $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration."); + + var mails = await MailRepo.FindAsync(filter, account, cancellationToken); + var lastSync = ImapEmailService.GetLastImapSyncDate(account.Id, filter.Folder); + + return new ReadEmailQueryResponse + { + LastSync = lastSync, + Emails = Mapper.Map>(mails) + }; + } + + private async Task HandlePop3Async(EmailAccount account, MailSearchFilter filter, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(account.Pop3Server)) + throw new BadRequestException( + $"POP3 is not configured for account '{account.Username}' (Id: {account.Id}). Set Pop3Server in EmailAccounts configuration."); + + // Trigger on-demand POP3 sync before querying local storage + await Pop3EmailService.SyncEmailsAsync(account, cancellationToken); + + // POP3 has no folder concept — always query INBOX + var pop3Filter = filter with { Folder = "INBOX" }; + var mails = await MailRepo.FindAsync(pop3Filter, account, cancellationToken); + var lastSync = Pop3EmailService.GetLastPop3SyncDate(account.Id); + + return new ReadEmailQueryResponse + { + LastSync = lastSync, + Emails = Mapper.Map>(mails) + }; + } +} +#endif diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaImapQuery.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaImapQuery.cs deleted file mode 100644 index e6e2454..0000000 --- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaImapQuery.cs +++ /dev/null @@ -1,58 +0,0 @@ -#if NET -using AutoMapper; -using DigitalData.MessagingService.Application.Common.Dto; -using DigitalData.MessagingService.Application.Common.Dto.MailSearch; -using DigitalData.MessagingService.Application.Common.Interfaces; -using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; -using DigitalData.MessagingService.Application.EmailAccounts.Queries; -using DigitalData.MessagingService.Domain.Entities; -using DigitalData.MessagingService.Domain.Exceptions; -using MediatR; -using Microsoft.Extensions.Logging; - -namespace DigitalData.MessagingService.Application.EmailReceiving.Queries; - -/// -/// Query to fetch emails from an IMAP mailbox. -/// -public record ReadEmailViaImapQuery : IRequest -{ - /// - /// Identifies the email account to use. - /// - public required GetEmailAccountQuery Account { get; init; } - - /// - /// Mail query used to filter and limit the emails retrieved. - /// - public MailSearchFilter Mail { get; init; } = new(); -} - -public class ReadEmailViaImapQueryHandler(IMapper Mapper, ILogger Logger, IRepository EmailAccountRepo, IReceivedEmailRepository MailRepo, IImapEmailService imapEmailService) : IRequestHandler -{ - public async Task Handle(ReadEmailViaImapQuery request, CancellationToken cancellationToken) - { - var accounts = await EmailAccountRepo.FindAsync(request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username, cancellationToken: cancellationToken); - - if (accounts.Count() > 1) - Logger.LogWarning("Multiple email accounts found for the given criteria ({Criteria}). Returning the first one.", request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}"); - - var account = accounts.FirstOrDefault() - ?? throw new NotFoundException($"No email account found for the given criteria (Id: {request.Account.Id}, Username: {request.Account.Username})."); - - if (string.IsNullOrWhiteSpace(account.ImapServer)) - throw new BadRequestException( - $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration."); - - var mails = await MailRepo.FindAsync(request.Mail, account, cancellationToken); - - var lastSync = imapEmailService.GetLastImapSyncDate(account.Id, request.Mail.Folder); - - return new ReadEmailQueryResponse - { - LastSync = lastSync, - Emails = Mapper.Map>(mails) - }; - } -} -#endif \ No newline at end of file diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaOAuth2Query.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaOAuth2Query.cs deleted file mode 100644 index 51bf42c..0000000 --- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaOAuth2Query.cs +++ /dev/null @@ -1,77 +0,0 @@ -#if NET -using AutoMapper; -using DigitalData.MessagingService.Application.Common.Dto; -using DigitalData.MessagingService.Application.Common.Dto.MailSearch; -using DigitalData.MessagingService.Application.Common.Interfaces; -using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; -using DigitalData.MessagingService.Application.EmailAccounts.Queries; -using DigitalData.MessagingService.Application.EmailReceiving.Queries; -using DigitalData.MessagingService.Domain.Entities; -using DigitalData.MessagingService.Domain.Exceptions; -using MediatR; -using Microsoft.Extensions.Logging; - -namespace DigitalData.MessagingService.Application.EmailReceiving.Queries; - -/// -/// Query to fetch emails from an IMAP mailbox using OAuth2 authentication. -/// The account must have UseOAuth2 = true and valid OAuth2 credentials configured. -/// -public record ReadEmailViaOAuth2Query : IRequest -{ - /// - /// Identifies the email account to use. - /// - public required GetEmailAccountQuery Account { get; init; } - - /// - /// Mail query used to filter and limit the emails retrieved. - /// - public MailSearchFilter Mail { get; init; } = new(); -} - -public class ReadEmailViaOAuth2QueryHandler( - IMapper Mapper, - ILogger Logger, - IRepository EmailAccountRepo, - IReceivedEmailRepository MailRepo, - IImapEmailService imapEmailService) : IRequestHandler -{ - public async Task Handle(ReadEmailViaOAuth2Query request, CancellationToken cancellationToken) - { - var accounts = await EmailAccountRepo.FindAsync( - request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username, - cancellationToken: cancellationToken); - - if (accounts.Count() > 1) - Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.", - request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}"); - - var account = accounts.FirstOrDefault() - ?? throw new NotFoundException($"No email account found (Id: {request.Account.Id}, Username: {request.Account.Username})."); - - if (!account.UseOAuth2) - throw new BadRequestException( - $"Account '{account.Username}' (Id: {account.Id}) is not configured for OAuth2. Set UseOAuth2 = true."); - - if (string.IsNullOrWhiteSpace(account.OAuth2ClientId) || - string.IsNullOrWhiteSpace(account.OAuth2ClientSecret)) - throw new BadRequestException( - $"OAuth2 credentials (ClientId, ClientSecret) are not configured for account '{account.Username}'."); - - if (string.IsNullOrWhiteSpace(account.ImapServer)) - throw new BadRequestException( - $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration."); - - var mails = await MailRepo.FindAsync(request.Mail, account, cancellationToken); - - var lastSync = imapEmailService.GetLastImapSyncDate(account.Id, request.Mail.Folder); - - return new ReadEmailQueryResponse - { - LastSync = lastSync, - Emails = Mapper.Map>(mails) - }; - } -} -#endif diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaPop3Query.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaPop3Query.cs deleted file mode 100644 index 0f3bee0..0000000 --- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaPop3Query.cs +++ /dev/null @@ -1,74 +0,0 @@ -#if NET -using AutoMapper; -using DigitalData.MessagingService.Application.Common.Dto; -using DigitalData.MessagingService.Application.Common.Dto.MailSearch; -using DigitalData.MessagingService.Application.Common.Interfaces; -using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; -using DigitalData.MessagingService.Application.EmailAccounts.Queries; -using DigitalData.MessagingService.Application.EmailReceiving.Queries; -using DigitalData.MessagingService.Domain.Entities; -using DigitalData.MessagingService.Domain.Exceptions; -using MediatR; -using Microsoft.Extensions.Logging; - -namespace DigitalData.MessagingService.Application.EmailReceiving.Queries; - -/// -/// Query to fetch emails from a POP3 mailbox. -/// Triggers an on-demand sync and returns stored results filtered by . -/// -public record ReadEmailViaPop3Query : IRequest -{ - /// - /// Identifies the email account to use. - /// - public required GetEmailAccountQuery Account { get; init; } - - /// - /// Mail query used to filter and limit the emails retrieved from local storage. - /// Note: POP3 has no folder concept — all messages are stored under "INBOX". - /// - public MailSearchFilter Mail { get; init; } = new(); -} - -public class ReadEmailViaPop3QueryHandler( - IMapper Mapper, - ILogger Logger, - IRepository EmailAccountRepo, - IReceivedEmailRepository MailRepo, - IPop3EmailService pop3EmailService) : IRequestHandler -{ - public async Task Handle(ReadEmailViaPop3Query request, CancellationToken cancellationToken) - { - var accounts = await EmailAccountRepo.FindAsync( - request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username, - cancellationToken: cancellationToken); - - if (accounts.Count() > 1) - Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.", - request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}"); - - var account = accounts.FirstOrDefault() - ?? throw new NotFoundException($"No email account found (Id: {request.Account.Id}, Username: {request.Account.Username})."); - - if (string.IsNullOrWhiteSpace(account.Pop3Server)) - throw new BadRequestException( - $"POP3 is not configured for account '{account.Username}' (Id: {account.Id}). Set Pop3Server in EmailAccounts configuration."); - - // Trigger on-demand POP3 sync before querying local storage - await pop3EmailService.SyncEmailsAsync(account, cancellationToken); - - // POP3 has no folder concept — always query INBOX - var filter = request.Mail with { Folder = "INBOX" }; - var mails = await MailRepo.FindAsync(filter, account, cancellationToken); - - var lastSync = pop3EmailService.GetLastPop3SyncDate(account.Id); - - return new ReadEmailQueryResponse - { - LastSync = lastSync, - Emails = Mapper.Map>(mails) - }; - } -} -#endif diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailsQueryValidator.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailsQueryValidator.cs index e9e7919..4b49283 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailsQueryValidator.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailsQueryValidator.cs @@ -5,9 +5,9 @@ using FluentValidation; namespace DigitalData.MessagingService.Application.EmailReceiving.Validators; /// -/// Validates a before it is handled by . +/// Validates a before it is handled by . /// -public class FetchEmailsQueryValidator : AbstractValidator +public class FetchEmailsQueryValidator : AbstractValidator { public FetchEmailsQueryValidator() { diff --git a/src/presentation/DigitalData.MessagingService.API/Controllers/EmailController.cs b/src/presentation/DigitalData.MessagingService.API/Controllers/EmailController.cs index ba7b2fc..c39d8c1 100644 --- a/src/presentation/DigitalData.MessagingService.API/Controllers/EmailController.cs +++ b/src/presentation/DigitalData.MessagingService.API/Controllers/EmailController.cs @@ -1,37 +1,71 @@ -using DigitalData.MessagingService.Application.EmailSending.Commands; +using DigitalData.MessagingService.Application.Common.Dto; using DigitalData.MessagingService.Application.EmailReceiving.Queries; +using DigitalData.MessagingService.Application.EmailSending.Commands; using MediatR; using Microsoft.AspNetCore.Mvc; -using DigitalData.MessagingService.Application.Common.Dto; namespace DigitalData.MessagingService.API.Controllers; /// -/// Email sending API controller. -/// Enqueues outgoing emails to RabbitMQ for async processing. +/// Email API controller. +/// Handles sending and receiving emails across all supported protocols and auth methods. /// [ApiController] [Route("api/[controller]")] public class EmailController(IMediator mediator) : ControllerBase { /// - /// + /// Controls which field(s) to return from a fetch-emails response. /// public enum OnlyFilter { - /// - /// - /// + /// Returns only the HTML body of the first matching email. HtmlBody, - /// - /// - /// + /// Returns only the UID list of matching emails. Uid, } + #region Receive + /// + /// Fetch emails from an email account. + /// The protocol (IMAP, POP3) and authentication method (basic, OAuth2) are determined + /// by the IncomingProtocol field configured on the account. + /// + /// Query parameters for filtering and fetching emails. + /// Optional filter to return only specific fields. + /// Cancellation token. + /// HTTP 200 with list of received emails. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task FetchEmails( + [FromQuery] ReadEmailQuery query, + [FromQuery] OnlyFilter? only = null, + CancellationToken cancellationToken = default) + { + var res = await mediator.Send(query, cancellationToken); + + if (!res.Emails.Any()) + return NotFound("No emails found matching the specified criteria."); + + if (only == OnlyFilter.HtmlBody) + { + if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody) + return Content(htmlBody, "text/html"); + else + return NotFound(); + } + else if (only == OnlyFilter.Uid) + return Ok(res.Emails.Select(e => e.Uid).ToList()); + else + return Ok(res); + } + #endregion Receive + #region SMTP Send /// - /// Send an email, optionally with file attachments. + /// Send an email via SMTP, optionally with file attachments. /// Omit the attachments field for a plain send. /// /// Email fields as form values @@ -53,66 +87,8 @@ public class EmailController(IMediator mediator) : ControllerBase var eventId = await mediator.Send(commandWithAttachments, cancellationToken); return Accepted(new { Id = eventId }); } - - private static async Task> BuildAttachmentsAsync( - IFormFileCollection? files, - CancellationToken cancellationToken) - { - if (files is null || files.Count == 0) - return []; - - var result = new List(files.Count); - - foreach (var file in files) - { - using var ms = new MemoryStream(); - await file.CopyToAsync(ms, cancellationToken); - - result.Add(new EmailAttachmentDto - { - FileName = file.FileName, - Content = ms.ToArray(), - ContentType = file.ContentType - }); - } - - return result; - } #endregion SMTP Send - #region IMAP Receive - /// - /// Fetch emails from an IMAP mailbox. - /// - /// Query parameters for filtering and fetching emails from the IMAP mailbox. - /// - /// Cancellation token. - /// HTTP 200 with list of received emails. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task FetchEmails([FromQuery] ReadEmailViaImapQuery query, [FromQuery] OnlyFilter? only = null, CancellationToken cancellationToken = default) - { - var res = await mediator.Send(query, cancellationToken); - - if(!res.Emails.Any()) - return NotFound("No emails found matching the specified criteria."); - - if (only == OnlyFilter.HtmlBody) - { - if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody) - return Content(htmlBody, "text/html"); - else - return NotFound(); - } - else if (only == OnlyFilter.Uid) - return Ok(res.Emails.Select(e => e.Uid).ToList()); - else - return Ok(res); - } - #endregion IMAP Receive - #region IMAP Send /// /// Send an email using IMAP account credentials (via RabbitMQ queue). @@ -139,43 +115,6 @@ public class EmailController(IMediator mediator) : ControllerBase } #endregion IMAP Send - #region POP3 Receive - /// - /// Fetch emails from a POP3 mailbox. - /// Triggers an on-demand POP3 sync before returning results. - /// - /// Query parameters for filtering and fetching emails from the POP3 mailbox. - /// Optional filter to return only specific fields. - /// Cancellation token. - /// HTTP 200 with list of received emails. - [HttpGet("pop3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task FetchEmailsViaPop3( - [FromQuery] ReadEmailViaPop3Query query, - [FromQuery] OnlyFilter? only = null, - CancellationToken cancellationToken = default) - { - var res = await mediator.Send(query, cancellationToken); - - if (!res.Emails.Any()) - return NotFound("No emails found matching the specified criteria."); - - if (only == OnlyFilter.HtmlBody) - { - if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody) - return Content(htmlBody, "text/html"); - else - return NotFound(); - } - else if (only == OnlyFilter.Uid) - return Ok(res.Emails.Select(e => e.Uid).ToList()); - else - return Ok(res); - } - #endregion POP3 Receive - #region OAuth2 Send /// /// Send an email via SMTP using OAuth2 authentication (via RabbitMQ queue). @@ -202,40 +141,28 @@ public class EmailController(IMediator mediator) : ControllerBase } #endregion OAuth2 Send - #region OAuth2 Receive - /// - /// Fetch emails from an IMAP mailbox using OAuth2 authentication. - /// The account must have UseOAuth2 = true and valid OAuth2 credentials configured. - /// - /// Query parameters for filtering and fetching emails. - /// Optional filter to return only specific fields. - /// Cancellation token. - /// HTTP 200 with list of received emails. - [HttpGet("oauth2/imap")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task FetchEmailsViaOAuth2( - [FromQuery] ReadEmailViaOAuth2Query query, - [FromQuery] OnlyFilter? only = null, - CancellationToken cancellationToken = default) + private static async Task> BuildAttachmentsAsync( + IFormFileCollection? files, + CancellationToken cancellationToken) { - var res = await mediator.Send(query, cancellationToken); + if (files is null || files.Count == 0) + return []; - if (!res.Emails.Any()) - return NotFound("No emails found matching the specified criteria."); + var result = new List(files.Count); - if (only == OnlyFilter.HtmlBody) + foreach (var file in files) { - if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody) - return Content(htmlBody, "text/html"); - else - return NotFound(); + using var ms = new MemoryStream(); + await file.CopyToAsync(ms, cancellationToken); + + result.Add(new EmailAttachmentDto + { + FileName = file.FileName, + Content = ms.ToArray(), + ContentType = file.ContentType + }); } - else if (only == OnlyFilter.Uid) - return Ok(res.Emails.Select(e => e.Uid).ToList()); - else - return Ok(res); + + return result; } - #endregion OAuth2 Receive } diff --git a/src/presentation/DigitalData.MessagingService.API/DigitalData.MessagingService.API.csproj b/src/presentation/DigitalData.MessagingService.API/DigitalData.MessagingService.API.csproj index 688810a..dc2a075 100644 --- a/src/presentation/DigitalData.MessagingService.API/DigitalData.MessagingService.API.csproj +++ b/src/presentation/DigitalData.MessagingService.API/DigitalData.MessagingService.API.csproj @@ -10,9 +10,9 @@ Digital Data GmbH Digital Data GmbH DigitalData.MessagingService.API - 1.0.0-beta - 1.0.0.0 - 1.0.0.0 + 2.0.0-beta + 2.0.0.0 + 2.0.0.0 Copyright © 2026 Digital Data GmbH. All rights reserved. bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml