Skip to content

Commit bf8e351

Browse files
Support text content for attachments (#59)
1 parent 5ab3eff commit bf8e351

File tree

12 files changed

+444
-20
lines changed

12 files changed

+444
-20
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\Resend\Resend.csproj" />
15+
</ItemGroup>
16+
17+
</Project>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using Microsoft.Extensions.Options;
2+
using Resend;
3+
4+
5+
/*
6+
*
7+
*/
8+
var apiToken = Environment.GetEnvironmentVariable( "RESEND_APITOKEN" );
9+
10+
if ( apiToken == null )
11+
{
12+
Console.Error.WriteLine( "err: Environnment variable RESEND_APITOKEN is not defined" );
13+
return;
14+
}
15+
16+
17+
/*
18+
*
19+
*/
20+
var options = new ResendClientOptions()
21+
{
22+
ApiToken = apiToken,
23+
};
24+
25+
var resend = ResendClient.Create( options );
26+
27+
28+
/*
29+
* Warning: iCalendar content must be sent as text.
30+
*/
31+
var inviteText = File.ReadAllText( "invite.ics" );
32+
33+
34+
/*
35+
*
36+
*/
37+
try
38+
{
39+
var email = new EmailMessage()
40+
{
41+
42+
43+
Subject = "Hello from Console",
44+
HtmlBody = "<p>Email using <strong>Resend .NET SDK</strong></p>",
45+
Attachments = [
46+
new EmailAttachment()
47+
{
48+
Filename = "invite.ics",
49+
ContentType = "text/calendar",
50+
Content = inviteText,
51+
},
52+
],
53+
};
54+
55+
var resp = await resend.EmailSendAsync( email );
56+
57+
Console.WriteLine( "Id={0}", resp.Content );
58+
}
59+
catch ( ResendException ex )
60+
{
61+
Console.Error.WriteLine( "err: {0} - {1}", ex.ErrorType, ex.Message );
62+
}

examples/ConsoleCalendar/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.NET SDK: Console - iCalendar
2+
=====================================================================
3+
4+
This example shows how to send emails containing an iCalendar
5+
attachment, which requires the attachment to be provided as a
6+
string (rather than a byte[]).
7+
8+
9+
How to run
10+
---------------------------------------------------------------------
11+
12+
1. Set the `RESEND_APITOKEN` environment variable to your Resend API.
13+
2. Edit the `From` and `To` in the `Program.cs` as necessary.
14+
3. Run the console app with `dotnet run`.
15+
16+
```bash
17+
> set RESEND_APITOKEN=re_8m9gwsVG_6n94KaJkJ42Yj6qSeVvLq9xF
18+
> dotnet run
19+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
PRODID:-//YourApp//EN
4+
METHOD:REQUEST
5+
BEGIN:VEVENT
6+
7+
DTSTAMP:20250604T130000Z
8+
DTSTART:20250610T140000Z
9+
DTEND:20250610T150000Z
10+
SUMMARY:Team Sync Meeting
11+
DESCRIPTION:Weekly team sync-up.
12+
LOCATION:Conference Room A
13+
STATUS:CONFIRMED
14+
SEQUENCE:0
15+
BEGIN:VALARM
16+
TRIGGER:-PT15M
17+
ACTION:DISPLAY
18+
DESCRIPTION:Reminder
19+
END:VALARM
20+
END:VEVENT
21+
END:VCALENDAR

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
|----------|------|-------------------------------------------------------
66
| [AsyncHangfire](https://github.com/resend/resend-dotnet/tree/master/examples/AsyncHangfire) | 5003 | Send email as a background job using [Hangfire](https://www.hangfire.io/)
77
| [AsyncTemporal](https://github.com/resend/resend-dotnet/tree/master/examples/AsyncTemporal) | 5004 | Send email in durable workflow using [Temporal](https://temporal.io/)
8+
| [ConsoleCalendar](https://github.com/resend/resend-dotnet/tree/master/examples/ConsoleCalendar) | n/a | Send email iCalendar attachment from console app
89
| [ConsoleNoDi](https://github.com/resend/resend-dotnet/tree/master/examples/ConsoleNoDi) | n/a | Send email from console app (without dependency injection)
910
| [RenderLiquid](https://github.com/resend/resend-dotnet/tree/master/examples/RenderLiquid) | 5006 | Render an HTML body using [Fluid](https://github.com/sebastienros/fluid), a [Liquid](https://shopify.github.io/liquid/) template language
1011
| [RenderRazor](https://github.com/resend/resend-dotnet/tree/master/examples/RenderRazor) | 5005 | Render an HTML body using Razor views

resend-dotnet.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebControllerApi", "example
6262
EndProject
6363
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resend.DocsCheck", "tools\Resend.DocsCheck\Resend.DocsCheck.csproj", "{ED268270-FD7F-4111-8B54-020C81F8D683}"
6464
EndProject
65+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleCalendar", "examples\ConsoleCalendar\ConsoleCalendar.csproj", "{9B2A782F-D19D-BBDC-2B24-9F8735E07D40}"
66+
EndProject
6567
Global
6668
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6769
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +138,10 @@ Global
136138
{ED268270-FD7F-4111-8B54-020C81F8D683}.Debug|Any CPU.Build.0 = Debug|Any CPU
137139
{ED268270-FD7F-4111-8B54-020C81F8D683}.Release|Any CPU.ActiveCfg = Release|Any CPU
138140
{ED268270-FD7F-4111-8B54-020C81F8D683}.Release|Any CPU.Build.0 = Release|Any CPU
141+
{9B2A782F-D19D-BBDC-2B24-9F8735E07D40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
142+
{9B2A782F-D19D-BBDC-2B24-9F8735E07D40}.Debug|Any CPU.Build.0 = Debug|Any CPU
143+
{9B2A782F-D19D-BBDC-2B24-9F8735E07D40}.Release|Any CPU.ActiveCfg = Release|Any CPU
144+
{9B2A782F-D19D-BBDC-2B24-9F8735E07D40}.Release|Any CPU.Build.0 = Release|Any CPU
139145
EndGlobalSection
140146
GlobalSection(SolutionProperties) = preSolution
141147
HideSolutionNode = FALSE
@@ -158,6 +164,7 @@ Global
158164
{FD4407E3-551D-48F8-9FFB-63E409F4C4BB} = {121B647E-18F4-41CB-AC00-49D5F06D5320}
159165
{64049053-1C1E-43D6-A23B-5A609354934B} = {121B647E-18F4-41CB-AC00-49D5F06D5320}
160166
{ED268270-FD7F-4111-8B54-020C81F8D683} = {CAC9B80A-B362-4135-AC30-8013D0DB6124}
167+
{9B2A782F-D19D-BBDC-2B24-9F8735E07D40} = {121B647E-18F4-41CB-AC00-49D5F06D5320}
161168
EndGlobalSection
162169
GlobalSection(ExtensibilityGlobals) = postSolution
163170
SolutionGuid = {15EEB6F3-A067-45F5-987C-824BD8FDEAF9}

src/Resend.FluentEmail/ResendSender.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using FluentEmail.Core;
22
using FluentEmail.Core.Interfaces;
33
using FluentEmail.Core.Models;
4+
using System.Text;
45

56
namespace Resend.FluentEmail;
67

@@ -167,19 +168,53 @@ private static EmailMessage ToMessage( IFluentEmail email )
167168
var ms = new MemoryStream();
168169
x.Data.CopyTo( ms );
169170

170-
return new EmailAttachment()
171+
if ( AsText( x.ContentType ) == true )
171172
{
172-
Filename = x.Filename,
173-
ContentType = x.ContentType,
174-
Content = ms.ToArray(),
175-
};
173+
var text = Encoding.UTF8.GetString( ms.ToArray() );
174+
175+
return new EmailAttachment()
176+
{
177+
Filename = x.Filename,
178+
ContentType = x.ContentType,
179+
Content = ms.ToArray(),
180+
};
181+
}
182+
else
183+
{
184+
return new EmailAttachment()
185+
{
186+
Filename = x.Filename,
187+
ContentType = x.ContentType,
188+
Content = ms.ToArray(),
189+
};
190+
}
191+
176192
} ).ToList();
177193
}
178194

179195
return message;
180196
}
181197

182198

199+
/// <summary />
200+
private static bool AsText( string contentType )
201+
{
202+
if ( contentType.StartsWith( "text/" ) == true )
203+
return true;
204+
205+
if ( contentType == "application/json" )
206+
return true;
207+
208+
if ( contentType.EndsWith( "+json" ) == true )
209+
return true;
210+
211+
if ( contentType.EndsWith( "+xml" ) == true )
212+
return true;
213+
214+
return false;
215+
}
216+
217+
183218
/// <summary />
184219
private static EmailAddressList ToEmailAddressList( IList<Address> fluentAddresses )
185220
{

src/Resend/ByteArrayOrString.cs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Resend;
4+
5+
/// <summary>
6+
/// Discriminated union of byte array or string value, used to represent
7+
/// attachments into the Resend API.
8+
/// </summary>
9+
[JsonConverter( typeof( ByteArrayOrStringConverter ) )]
10+
public readonly struct ByteArrayOrString
11+
{
12+
private readonly byte[]? _bytes;
13+
private readonly string? _string;
14+
15+
16+
/// <summary />
17+
public ByteArrayOrString( byte[] value )
18+
{
19+
_bytes = value;
20+
_string = null;
21+
}
22+
23+
24+
/// <summary />
25+
public ByteArrayOrString( string value )
26+
{
27+
_string = value;
28+
_bytes = null;
29+
}
30+
31+
32+
/// <summary>
33+
/// Gets whether the value is a byte array.
34+
/// </summary>
35+
public bool IsByteArray { get => _bytes != null; }
36+
37+
/// <summary>
38+
/// Gets whether the value is a string.
39+
/// </summary>
40+
public bool IsString { get => _string != null; }
41+
42+
43+
/// <summary>
44+
/// Gets the byte array value, or null.
45+
/// </summary>
46+
public byte[]? ByteArray { get => _bytes; }
47+
48+
49+
/// <summary>
50+
/// Gets the string value, or null.
51+
/// </summary>
52+
public string? String { get => _string; }
53+
54+
55+
/// <summary>
56+
/// Gets the byte array value.
57+
/// </summary>
58+
/// <returns>
59+
/// Byte array value.
60+
/// </returns>
61+
/// <exception cref="InvalidOperationException">Thrown if value isn't byte array.</exception>
62+
public byte[] AsByteArray()
63+
{
64+
if ( _bytes is not null )
65+
return _bytes;
66+
67+
throw new InvalidOperationException( "Value is not a byte array." );
68+
}
69+
70+
/// <summary>
71+
/// Gets the string value.
72+
/// </summary>
73+
/// <returns>
74+
/// String value.
75+
/// </returns>
76+
/// <exception cref="InvalidOperationException">Thrown if value isn't string.</exception>
77+
public string AsString()
78+
{
79+
if ( _string is not null )
80+
return _string;
81+
82+
throw new InvalidOperationException( "Value is not a string." );
83+
}
84+
85+
86+
/// <summary>
87+
/// Gets a 32-bit integer that represents the total number of bytes (if value is
88+
/// a byte array), or the number of characters (if value is a string).
89+
/// </summary>
90+
public int Length
91+
{
92+
get
93+
{
94+
if ( _bytes != null )
95+
return _bytes.Length;
96+
97+
if ( _string != null )
98+
return _string.Length;
99+
100+
return 0;
101+
}
102+
}
103+
104+
105+
/// <summary>
106+
/// Gets a 64-bit integer that represents the total number of bytes (if value is
107+
/// a byte array), or the number of characters (if value is a string).
108+
/// </summary>
109+
public long LongLength
110+
{
111+
get
112+
{
113+
if ( _bytes != null )
114+
return _bytes.LongLength;
115+
116+
if ( _string != null )
117+
return _string.Length;
118+
119+
return 0;
120+
}
121+
}
122+
123+
124+
/// <inheritdoc />
125+
public override string ToString()
126+
{
127+
if ( _bytes != null )
128+
return $"[byte[]: {_bytes.LongLength} bytes]";
129+
130+
if ( _string != null )
131+
return _string;
132+
133+
return "(empty)";
134+
}
135+
136+
137+
/// <summary>
138+
/// Implicitly cast, if possible, to a byte array.
139+
/// </summary>
140+
public static implicit operator byte[]( ByteArrayOrString value )
141+
{
142+
if ( value.ByteArray != null )
143+
return value.ByteArray;
144+
145+
throw new InvalidOperationException( "BA002: Value is not a byte[]" );
146+
}
147+
148+
149+
/// <summary />
150+
public static implicit operator ByteArrayOrString( byte[] value )
151+
{
152+
return new ByteArrayOrString( value );
153+
}
154+
155+
156+
/// <summary />
157+
public static implicit operator ByteArrayOrString( string value )
158+
{
159+
return new ByteArrayOrString( value );
160+
}
161+
}

0 commit comments

Comments
 (0)