full text search fuzzy
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
# Email - SMTP Email with Zipped Log Attachments
This skill provides patterns for sending emails via SMTP with automatic log file compression and multiple attachment handling.
## The Problem
Sending large log files as email attachments causes issues:
- **Email size limits** - Most SMTP servers reject messages > 25-50MB
- **Log files can be huge** - Daily logs with verbose output grow quickly
- **File locking issues** - Log files are often locked by running processes
- **Disk cleanup** - Temporary zip files need cleanup after sending
## The Solution
A battle-tested pattern that:
1. Reads locked log files using `FileShare.ReadWrite`
2. Compresses logs into temporary zip files
3. Attaches both zipped logs and additional files
4. Cleans up temporary files in `finally` blocks
5. Falls back gracefully on errors
## Pattern 1: Basic Email Sending
```csharp
using System.Net.Mail;
public class Email
{
private readonly string _to;
private readonly string _subject;
private readonly string _body;
public Email(string to, string subject, string body)
{
_to = to;
_subject = subject;
_body = body;
}
public bool Send(string? from = null)
{
try
{
var smtpClient = new SmtpClient("smtp.nisainternational.local");
var mailMessage = new MailMessage(
from ?? "services@ramsden-international.com",
_to,
_subject,
_body
);
smtpClient.Send(mailMessage);
return true;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
}
```
**Usage:**
```csharp
var email = new Email(
"recipient@company.com",
"Daily Report",
"Processing completed successfully"
);
email.Send();
```
## Pattern 2: Email with Simple Attachments
```csharp
public bool Send(string? from, string? attachmentPath)
{
try
{
var smtpClient = new SmtpClient("smtp.nisainternational.local");
using var mailMessage = new MailMessage( // ← IMPORTANT: using statement for disposal
from ?? "services@ramsden-international.com",
_to,
_subject,
_body
);
if (!string.IsNullOrEmpty(attachmentPath) && File.Exists(attachmentPath))
{
mailMessage.Attachments.Add(new Attachment(attachmentPath)); // ← Create inline
}
smtpClient.Send(mailMessage);
return true;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
```
## Pattern 3: Zipped Log Attachments (The Key Pattern!)
This is the important one - handles locked files and compression:
```csharp
using System.IO.Compression;
public bool SendWithZippedAttachment(string? from, string? attachmentPath, bool deleteOriginal = false)
{
if (string.IsNullOrEmpty(attachmentPath))
{
Console.WriteLine("No attachment path provided, sending without attachment");
return Send(from);
}
if (!File.Exists(attachmentPath))
{
Console.WriteLine(quot;Attachment file not found: {attachmentPath}, sending without attachment");
return Send(from);
}
string? zippedPath = null;
bool emailSent = false;
try
{
Console.WriteLine(quot;Creating zip for {attachmentPath}");
zippedPath = ZipSingleFileFromStream(attachmentPath);
Console.WriteLine(quot;Zip created at {zippedPath}");
emailSent = Send(from, zippedPath);
Console.WriteLine(quot;Email sent with zip attachment: {emailSent}");
if (deleteOriginal && File.Exists(attachmentPath))
{
File.Delete(attachmentPath);
Console.WriteLine(quot;Original file deleted: {attachmentPath}");
}
return emailSent;
}
catch (Exception ex)
{
Console.WriteLine(quot;Error zipping attachment: {ex.Message}");
Console.WriteLine(quot;Falling back to original file attachment");
return Send(from, attachmentPath);
}
finally
{
// CRITICAL: Clean up zip file after email is sent
if (!string.IsNullOrEmpty(zippedPath) && File.Exists(zippedPath))
{
try
{
File.Delete(zippedPath);
Console.WriteLine(quot;Zip file cleaned up: {zippedPath}");
}
catch (Exception ex)
{
Console.WriteLine(quot;Failed to clean up zip file: {ex.Message}");
}
}
}
}
/// <summary>
/// The SECRET SAUCE: Opens file with FileShare.ReadWrite to read locked log files
/// </summary>
public static string ZipSingleFileFromStream(string filePath, string? outputPath = null)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException(quot;File not found: {filePath}");
}
string zipPath = outputPath ?? Path.ChangeExtension(filePath, ".zip");
if (File.Exists(zipPath))
{
File.Delete(zipPath);
}
using (var zipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Create))
{
var entry = zipArchive.CreateEntry(Path.GetFileName(filePath), CompressionLevel.Optimal);
using (var entryStream = entry.Open())
using (var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite)) // ← THIS IS KEY! Allows reading locked files
{
fileStream.CopyTo(entryStream);
}
}
return zipPath;
}
```
**Why FileShare.ReadWrite matters:**
- Log files are often **locked by running processes** (logging frameworks keep them open)
- `FileShare.ReadWrite` tells Windows "I want to read this file even if another process has it open for writing"
- Without this, you get `IOException: The process cannot access the file because it is being used by another process`
## Pattern 4: Multiple Attachments (Zipped Log + Additional Files)
For reports that need both compressed logs AND supplemental files:
```csharp
public bool SendWithZippedAttachmentAndAdditionalFiles(
string? from,
string? logFileToZip,
bool deleteOriginal = false,
params string[] additionalAttachments)
{
string? zippedPath = null;
bool emailSent = false;
try
{
var smtpClient = new SmtpClient("smtp.nisainternational.local");
using var mailMessage = new MailMessage( // ← CRITICAL: using statement for disposal!
from ?? "services@ramsden-international.com",
_to,
_subject,
_body
);
// Add zipped log file if provided
if (!string.IsNullOrEmpty(logFileToZip) && File.Exists(logFileToZip))
{
Console.WriteLine(quot;Creating zip for {logFileToZip}");
zippedPath = ZipSingleFileFromStream(logFileToZip);
Console.WriteLine(quot;Zip created at {zippedPath}");
if (File.Exists(zippedPath))
{
mailMessage.Attachments.Add(new Attachment(zippedPath)); // ← Create inline
}
if (deleteOriginal)
{
File.Delete(logFileToZip);
Console.WriteLine(quot;Original file deleted: {logFileToZip}");
}
}
// Add additional attachments (e.g., CSV files, reports)
foreach (var attachmentPath in additionalAttachments)
{
if (!string.IsNullOrEmpty(attachmentPath) && File.Exists(attachmentPath))
{
mailMessage.Attachments.Add(new Attachment(attachmentPath)); // ← Create inline
Console.WriteLine(quot;Added additional attachment: {attachmentPath}");
}
}
smtpClient.Send(mailMessage);
emailSent = true;
Console.WriteLine(quot;Email sent successfully");
return true;
}
catch (Exception ex)
{
Console.WriteLine(quot;Error sending email: {ex.Message}");
return false;
}
finally
{
// Clean up zip file after email is sent
// Note: With using statement above, file handles are released before we get here
if (!string.IsNullOrEmpty(zippedPath) && File.Exists(zippedPath))
{
try
{
File.Delete(zippedPath);
Console.WriteLine(quot;Zip file cleaned up: {zippedPath}");
}
catch (Exception ex)
{
Console.WriteLine(quot;Failed to clean up zip file: {ex.Message}");
}
}
}
}
```
**Usage:**
```csharp
var email = new Email(
"manager@company.com",
"Daily Processing Report",
"See attached logs and missing items report"
);
// Send with zipped log + CSV of missing barcodes
email.SendWithZippedAttachmentAndAdditionalFiles(
null, // from (use default)
"C:\\Logs\\app_2025_11_03.log", // log to zip
false, // don't delete original
"C:\\Reports\\MissingBarcodes_2025_11_03.txt" // additional file
);
```
## Real-World Example: Daily Report with Logs
```csharp
public void SendDailyReport(List<string> processingMessages, string logFilePath)
{
// Generate summary
int totalProcessed = processingMessages.Count(m => m.Contains("SUCCESS"));
int errors = processingMessages.Count(m => m.Contains("ERROR"));
var summary = $@"Daily Processing Report
========================
Total Items Processed: {totalProcessed}
Errors Encountered: {errors}
For detailed logs, see attached file.
Execution completed at: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
// Create missing items file if needed
var missingItems = processingMessages
.Where(m => m.Contains("No images found"))
.ToList();
string? missingItemsPath = null;
if (missingItems.Any())
{
missingItemsPath = Path.Combine(
Path.GetDirectoryName(logFilePath) ?? "C:\\Logs",
quot;MissingItems_{DateTime.Now:yyyy_MM_dd}.txt"
);
File.WriteAllText(missingItemsPath, string.Join("\n", missingItems));
Console.WriteLine(quot;Created missing items file: {missingItemsPath}");
}
// Send email with zipped log + missing items file
var email = new Email(
"operations@company.com",
"Daily Processing Report",
summary
);
if (!string.IsNullOrEmpty(missingItemsPath))
{
email.SendWithZippedAttachmentAndAdditionalFiles(
null,
logFilePath,
false,
missingItemsPath
);
// Clean up missing items file after sending
try
{
File.Delete(missingItemsPath);
Console.WriteLine(quot;Cleaned up: {missingItemsPath}");
}
catch (Exception ex)
{
Console.WriteLine(quot;Failed to clean up: {ex.Message}");
}
}
else
{
email.SendWithZippedAttachment(null, logFilePath, false);
}
}
```
## Configuration for Different Environments
### Development Environment
```csharp
// Use local pickup directory for testing (no actual SMTP)
var smtpClient = new SmtpClient
{
DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
PickupDirectoryLocation = @"C:\EmailPickup"
};
```
### Production Environment
```csharp
// Standard SMTP server
var smtpClient = new SmtpClient("smtp.nisainternational.local");
```
### External SMTP (Gmail, Office 365, etc.)
```csharp
var smtpClient = new SmtpClient("smtp.gmail.com")
{
Port = 587,
Credentials = new NetworkCredential("user@gmail.com", "app-password"),
EnableSsl = true
};
```
## Common Pitfalls and Solutions
### Pitfall 1: Forgetting to Clean Up Zip Files
**Problem:** Disk fills up with temporary .zip files
**Solution:** Always use `finally` blocks to delete temporary files
```csharp
finally
{
if (!string.IsNullOrEmpty(zippedPath) && File.Exists(zippedPath))
{
File.Delete(zippedPath);
}
}
```
### Pitfall 2: File Locking Issues
**Problem:** `IOException: The process cannot access the file`
**Solution:** Use `FileShare.ReadWrite` when opening files
```csharp
using (var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite)) // ← Allows reading locked files
```
### Pitfall 3: Large Attachment Failures
**Problem:** Email server rejects messages > 25MB
**Solution:** Compression helps, but also check limits
```csharp
var fileInfo = new FileInfo(zipPath);
if (fileInfo.Length > 25 * 1024 * 1024) // 25MB
{
Console.WriteLine(quot;Warning: Attachment is {fileInfo.Length / (1024.0 * 1024.0):F2}MB");
// Consider splitting into multiple emails or using cloud storage links
}
```
### Pitfall 4: SMTP Authentication Errors
**Problem:** Email fails silently with no clear error
**Solution:** Add detailed logging
```csharp
try
{
Console.WriteLine(quot;Connecting to SMTP server...");
smtpClient.Send(mailMessage);
Console.WriteLine(quot;Email sent successfully to {_to}");
}
catch (SmtpException ex)
{
Console.WriteLine(quot;SMTP Error: {ex.StatusCode} - {ex.Message}");
throw;
}
```
### Pitfall 5: Undisposed MailMessage and Attachment Objects (CRITICAL!)
**Problem:** Files remain locked after email is sent, even after 10+ minutes
**Root Cause:** `MailMessage` and `Attachment` objects hold file handles that aren't released until garbage collection
**Symptom:** `IOException: The process cannot access the file because it is being used by another process` when trying to delete temp files
**WRONG WAY (leaks file handles):**
```csharp
var mailMessage = new MailMessage(from, to, subject, body);
if (File.Exists(attachmentPath))
{
var attachment = new Attachment(attachmentPath); // Opens file handle
mailMessage.Attachments.Add(attachment);
}
smtpClient.Send(mailMessage);
// File handles still open! Won't be released until GC runs
```
**RIGHT WAY (properly disposes):**
```csharp
using var mailMessage = new MailMessage(from, to, subject, body); // ← using statement!
if (File.Exists(attachmentPath))
{
mailMessage.Attachments.Add(new Attachment(attachmentPath)); // ← Create inline, no variable
}
smtpClient.Send(mailMessage);
// When using block exits, mailMessage.Dispose() is called
// This automatically disposes all attachments in the collection
// File handles are immediately released
```
**Why This Matters:**
1. `Attachment` objects open file handles when created
2. Without `using`, those handles stay open indefinitely (until GC runs)
3. You can't delete files while handles are open
4. Even with retry logic (5 attempts × 60 seconds), files stay locked
5. `MailMessage.Dispose()` automatically disposes all attachments in its `Attachments` collection
**Key Principle:**
- Create `Attachment` objects **inline** (don't store in variables)
- Let `MailMessage.Attachments` collection own them
- Use `using` on `MailMessage` to ensure disposal
- When `MailMessage` is disposed, all attachments are disposed too
## Required NuGet Packages
None! This uses built-in .NET libraries:
- `System.Net.Mail` (SMTP)
- `System.IO.Compression` (Zipping)
## Performance Tips
1. **Compression is fast** - Don't skip it even for "small" files (text compresses well)
2. **Use async for large batches** - If sending 10+ emails, consider `SendAsync()`
3. **Reuse SmtpClient carefully** - It's not thread-safe, use one per thread
4. **Dispose properly** - `MailMessage` and `Attachment` objects hold file handles
## Integration with UTF-8 Logging
```csharp
// Get current log file path from UTF8Writer
var logPath = UTF8Writer.GetCurrentLogFilePath();
// Send daily report with log attached
var email = new Email(
"ops@company.com",
"Daily Report",
GenerateReportSummary()
);
email.SendWithZippedAttachment(null, logPath, deleteOriginal: false);
```
## Testing Strategy
```csharp
// 1. Test basic sending
var email = new Email("test@company.com", "Test", "Body");
Assert.True(email.Send());
// 2. Test with attachment
var testFile = "test.txt";
File.WriteAllText(testFile, "Test content");
Assert.True(email.Send(null, testFile));
// 3. Test with zipped attachment
var largeTestFile = "test.log";
File.WriteAllText(largeTestFile, new string('x', 10_000_000)); // 10MB
Assert.True(email.SendWithZippedAttachment(null, largeTestFile));
Assert.False(File.Exists(Path.ChangeExtension(largeTestFile, ".zip"))); // Verify cleanup
// 4. Test with locked file
using (var fs = File.Open(largeTestFile, FileMode.Open, FileAccess.Write, FileShare.Read))
{
// File is locked by this process
Assert.True(email.SendWithZippedAttachment(null, largeTestFile)); // Should still work!
}
```
## When to Use Each Method
| Method | Use Case |
|--------|----------|
| `Send()` | Simple notifications, no attachments |
| `Send(from, attachmentPath)` | Small files (< 5MB), not actively locked |
| `SendWithZippedAttachment()` | **Log files, large text files, locked files** |
| `SendWithZippedAttachmentAndAdditionalFiles()` | **Daily reports with logs + CSV/Excel reports** |
## Pro Tips
1. **Always compress logs** - 10MB log → 500KB zip (20x reduction!)
2. **Use descriptive zip names** - `app_2025_11_03.zip` not `temp.zip`
3. **Include timestamp in subjects** - `"Daily Report - 2025-11-03"` helps filtering
4. **Monitor email delivery** - Check return value, log failures
5. **Fallback gracefully** - If zip fails, send uncompressed (better than no email)
## Example: Complete Daily Report System
```csharp
public class DailyReportMailer
{
private readonly string _recipient;
private readonly string _logDirectory;
public DailyReportMailer(string recipient, string logDirectory)
{
_recipient = recipient;
_logDirectory = logDirectory;
}
public void SendDailyReport(string reportSummary, List<string> errorMessages)
{
var today = DateTime.Now.ToString("yyyy-MM-dd");
var logPath = Path.Combine(_logDirectory, quot;app_{DateTime.Now:yyyy_MM_dd}.log");
// Create error report if there are errors
string? errorReportPath = null;
if (errorMessages.Any())
{
errorReportPath = Path.Combine(_logDirectory, quot;errors_{today}.txt");
File.WriteAllText(errorReportPath,
quot;Errors on {today}\n" +
quot;Total: {errorMessages.Count}\n\n" +
string.Join("\n", errorMessages));
}
var email = new Email(
_recipient,
quot;Daily Report - {today}",
reportSummary
);
try
{
if (!string.IsNullOrEmpty(errorReportPath))
{
email.SendWithZippedAttachmentAndAdditionalFiles(
null,
logPath,
deleteOriginal: false,
errorReportPath
);
// Clean up error report
File.Delete(errorReportPath);
}
else
{
email.SendWithZippedAttachment(null, logPath, deleteOriginal: false);
}
Console.WriteLine(quot;Daily report sent successfully to {_recipient}");
}
catch (Exception ex)
{
Console.WriteLine(quot;Failed to send daily report: {ex.Message}");
// Don't throw - logging should continue even if email fails
}
}
}
```
## Notes
- **FileShare.ReadWrite is the key trick** for reading locked log files
- **Always clean up zip files** in `finally` blocks
- **Fallback gracefully** - if zip fails, try sending original
- **Compression is worth it** - 10-50x size reduction for logs
- **Test with locked files** - that's the real-world scenario
This pattern has been battle-tested in production for daily automated reports!