// Copyright (c) 2026 Duplicati Inc. All rights reserved. using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; using Google.Apis.Gmail.v1; using Google.Apis.Gmail.v1.Data; using Google.Apis.Requests; using System.Runtime.CompilerServices; namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; /// /// Represents a folder containing conversations (emails) from a Google Group. /// Uses the Gmail API to search for emails sent to the group address across all member mailboxes. /// internal class GroupConversationsSourceEntry(SourceProvider provider, string parentPath, string groupEmail) : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, "conversations")), null, null) { public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) { // Get group members to find one mailbox to search var memberEmails = await GetGroupMemberEmailsAsync(cancellationToken); // Pick the first member, or fall back to the admin's mailbox var userToSearch = memberEmails.Append("me").First(); await foreach (var entry in EnumerateMessagesForUser(userToSearch, cancellationToken)) yield return entry; } /// /// Gets the email addresses of all group members. /// private async Task> GetGroupMemberEmailsAsync(CancellationToken cancellationToken) { var memberEmails = new List(); try { var service = provider.ApiHelper.GetDirectoryServiceForGroups(); var request = service.Members.List(groupEmail); string? nextPageToken = null; do { if (cancellationToken.IsCancellationRequested) break; request.PageToken = nextPageToken; var response = await request.ExecuteAsync(cancellationToken); if (response.MembersValue != null) { foreach (var member in response.MembersValue) { // Only include USER type members (not groups or other entity types) // and members with an email address if (member.Type == "USER" && !string.IsNullOrEmpty(member.Email)) { memberEmails.Add(member.Email); } } } nextPageToken = response.NextPageToken; } while (!string.IsNullOrEmpty(nextPageToken)); } catch (Exception) { // If we can't get members, we'll fall back to searching the admin's mailbox } return memberEmails; } /// /// Enumerates messages for a specific user that were sent to the group. /// private async IAsyncEnumerable EnumerateMessagesForUser( string userId, [EnumeratorCancellation] CancellationToken cancellationToken) { GmailService service; try { service = provider.ApiHelper.GetGmailService(userId); } catch { // If we can't access this user's mailbox, skip them yield break; } // Search for emails sent to the group address var request = service.Users.Messages.List(userId); request.Q = $"to:{groupEmail} OR cc:{groupEmail}"; do { if (cancellationToken.IsCancellationRequested) yield break; ListMessagesResponse response; try { response = await request.ExecuteAsync(cancellationToken); } catch { // If we can't search this user's mailbox, skip them yield break; } if (response.Messages != null) { var batch = new BatchRequest(service); var messages = new List(); foreach (var msg in response.Messages) { var msgRequest = service.Users.Messages.Get(userId, msg.Id); msgRequest.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Metadata; batch.Queue(msgRequest, (message, error, index, httpResponse) => { if (error == null) { lock (messages) { messages.Add(message); } } }); } if (batch.Count > 0) { await batch.ExecuteAsync(cancellationToken); } foreach (var message in messages) { if (cancellationToken.IsCancellationRequested) yield break; yield return new GmailMessageSourceEntry(userId, this.Path, message, service, groupEmail); } } request.PageToken = response.NextPageToken; } while (!string.IsNullOrEmpty(request.PageToken)); } public override Task> GetMinorMetadata(CancellationToken cancellationToken) { return Task.FromResult(new Dictionary { { "gsuite:v", "1" }, { "gsuite:Type", SourceItemType.GroupConversations.ToString() }, { "gsuite:Name", "conversations" }, { "gsuite:Id", groupEmail } } .Where(kv => !string.IsNullOrEmpty(kv.Value)) .ToDictionary(kv => kv.Key, kv => kv.Value)); } }