|  | 
|  | 1 | +using System; | 
| 1 | 2 | using System.Diagnostics; | 
| 2 | 3 | using System.Net; | 
| 3 | 4 | using System.Net.Http.Headers; | 
|  | 5 | +using System.Net.Http.Json; | 
|  | 6 | +using System.Net.Mail; | 
| 4 | 7 | using System.Text.Json; | 
| 5 | 8 | using Altinn.App.Core.Configuration; | 
| 6 | 9 | using Altinn.App.Core.Constants; | 
| 7 | 10 | using Altinn.App.Core.Features.Correspondence.Exceptions; | 
| 8 | 11 | using Altinn.App.Core.Features.Correspondence.Models; | 
|  | 12 | +using Altinn.App.Core.Features.Correspondence.Models.Response; | 
| 9 | 13 | using Altinn.App.Core.Models; | 
|  | 14 | +using Altinn.App.Core.Models.Notifications.Email; | 
| 10 | 15 | using Microsoft.AspNetCore.Mvc; | 
| 11 | 16 | using Microsoft.Extensions.Logging; | 
| 12 | 17 | using Microsoft.Extensions.Options; | 
| @@ -50,6 +55,70 @@ public async Task<SendCorrespondenceResponse> Send( | 
| 50 | 55 | 
 | 
| 51 | 56 |         try | 
| 52 | 57 |         { | 
|  | 58 | +            if ( | 
|  | 59 | +                payload.CorrespondenceRequest.Content.Attachments?.Count > 0 | 
|  | 60 | +                && payload.CorrespondenceRequest.Content.Attachments.All(a => a is CorrespondenceStreamedAttachment) | 
|  | 61 | +            ) | 
|  | 62 | +            { | 
|  | 63 | +                var pollingJobs = new List<Task>(); | 
|  | 64 | +                var premadeAttachments = new List<Guid>(); | 
|  | 65 | +                foreach ( | 
|  | 66 | +                    CorrespondenceStreamedAttachment attachment in payload.CorrespondenceRequest.Content.Attachments | 
|  | 67 | +                ) | 
|  | 68 | +                { | 
|  | 69 | +                    var initializeAttachmentPayload = new AttachmentPayload | 
|  | 70 | +                    { | 
|  | 71 | +                        DisplayName = attachment.Filename, | 
|  | 72 | +                        FileName = attachment.Filename, | 
|  | 73 | +                        IsEncrypted = attachment.IsEncrypted ?? false, | 
|  | 74 | +                        ResourceId = payload.CorrespondenceRequest.ResourceId, | 
|  | 75 | +                        SendersReference = payload.CorrespondenceRequest.SendersReference + " - " + attachment.Filename, | 
|  | 76 | +                    }; | 
|  | 77 | +                    var initializeAttachmentRequest = await AuthenticatedHttpRequestFactory( | 
|  | 78 | +                        method: HttpMethod.Post, | 
|  | 79 | +                        uri: GetUri("attachment"), | 
|  | 80 | +                        content: new StringContent( | 
|  | 81 | +                            JsonSerializer.Serialize(initializeAttachmentPayload), | 
|  | 82 | +                            new MediaTypeHeaderValue("application/json") | 
|  | 83 | +                        ), | 
|  | 84 | +                        payload: payload | 
|  | 85 | +                    ); | 
|  | 86 | +                    var initializeAttachmentResponse = await HandleServerCommunication<SendCorrespondenceResponse>( | 
|  | 87 | +                        initializeAttachmentRequest, | 
|  | 88 | +                        cancellationToken | 
|  | 89 | +                    ); | 
|  | 90 | +                    var initializeAttachmentContent = initializeAttachmentRequest.Content; | 
|  | 91 | +                    if (initializeAttachmentContent is null) | 
|  | 92 | +                    { | 
|  | 93 | +                        throw new CorrespondenceRequestException( | 
|  | 94 | +                            "Attachment initialization request did not return content.", | 
|  | 95 | +                            null, | 
|  | 96 | +                            HttpStatusCode.InternalServerError, | 
|  | 97 | +                            "No content returned from attachment initialization" | 
|  | 98 | +                        ); | 
|  | 99 | +                    } | 
|  | 100 | +                    var attachmentId = await initializeAttachmentContent.ReadAsStringAsync(cancellationToken); | 
|  | 101 | +                    attachmentId = attachmentId.Trim('"'); | 
|  | 102 | +                    var attachmentDataContent = new StreamContent(attachment.Data); | 
|  | 103 | +                    attachmentDataContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); | 
|  | 104 | +                    var uploadAttachmentRequest = await AuthenticatedHttpRequestFactory( | 
|  | 105 | +                        method: HttpMethod.Post, | 
|  | 106 | +                        uri: GetUri("attachment/upload"), | 
|  | 107 | +                        content: attachmentDataContent, | 
|  | 108 | +                        payload: payload | 
|  | 109 | +                    ); | 
|  | 110 | +                    var uploadAttachmentResponse = await HandleServerCommunication<SendCorrespondenceResponse>( | 
|  | 111 | +                        uploadAttachmentRequest, | 
|  | 112 | +                        cancellationToken | 
|  | 113 | +                    ); | 
|  | 114 | +                    if (Guid.TryParse(attachmentId, out var guidId)) | 
|  | 115 | +                    { | 
|  | 116 | +                        pollingJobs.Add(PollAttachmentStatus(guidId, payload, cancellationToken)); | 
|  | 117 | +                    } | 
|  | 118 | +                } | 
|  | 119 | +                await Task.WhenAll(pollingJobs); | 
|  | 120 | +                payload.CorrespondenceRequest.ExistingAttachments = premadeAttachments; | 
|  | 121 | +            } | 
| 53 | 122 |             using MultipartFormDataContent content = payload.CorrespondenceRequest.Serialise(); | 
| 54 | 123 |             using HttpRequestMessage request = await AuthenticatedHttpRequestFactory( | 
| 55 | 124 |                 method: HttpMethod.Post, | 
| @@ -88,6 +157,47 @@ public async Task<SendCorrespondenceResponse> Send( | 
| 88 | 157 |         } | 
| 89 | 158 |     } | 
| 90 | 159 | 
 | 
|  | 160 | +    /// <inheritdoc /> | 
|  | 161 | +    private async Task PollAttachmentStatus( | 
|  | 162 | +        Guid attachmentId, | 
|  | 163 | +        CorrespondencePayloadBase payload, | 
|  | 164 | +        CancellationToken cancellationToken | 
|  | 165 | +    ) | 
|  | 166 | +    { | 
|  | 167 | +        const int maxAttempts = 3; | 
|  | 168 | +        const int delaySeconds = 5; | 
|  | 169 | + | 
|  | 170 | +        for (int attempt = 1; attempt <= maxAttempts; attempt++) | 
|  | 171 | +        { | 
|  | 172 | +            await Task.Delay(TimeSpan.FromSeconds(delaySeconds), cancellationToken); | 
|  | 173 | + | 
|  | 174 | +            var statusResponse = await AuthenticatedHttpRequestFactory( | 
|  | 175 | +                method: HttpMethod.Get, | 
|  | 176 | +                uri: GetUri($"attachment/{attachmentId}"), | 
|  | 177 | +                content: null, | 
|  | 178 | +                payload: payload | 
|  | 179 | +            ); | 
|  | 180 | +            var pollContent = statusResponse.Content; | 
|  | 181 | + | 
|  | 182 | +            if (pollContent is null) | 
|  | 183 | +            { | 
|  | 184 | +                break; | 
|  | 185 | +            } | 
|  | 186 | + | 
|  | 187 | +            var statusContent = await pollContent.ReadFromJsonAsync<AttachmentOverview>(cancellationToken); | 
|  | 188 | +            if (statusContent?.Status == "Published") | 
|  | 189 | +            { | 
|  | 190 | +                return; | 
|  | 191 | +            } | 
|  | 192 | +        } | 
|  | 193 | +        throw new CorrespondenceRequestException( | 
|  | 194 | +            $"Failure when uploading attachment. Attachment was not published in time.", | 
|  | 195 | +            null, | 
|  | 196 | +            HttpStatusCode.InternalServerError, | 
|  | 197 | +            "Polling failed" | 
|  | 198 | +        ); | 
|  | 199 | +    } | 
|  | 200 | + | 
| 91 | 201 |     /// <inheritdoc/> | 
| 92 | 202 |     public async Task<GetCorrespondenceStatusResponse> GetStatus( | 
| 93 | 203 |         GetCorrespondenceStatusPayload payload, | 
|  | 
0 commit comments