feat(filing): scaffold Task #3 Feature 1 — file mail to Case/Contact

Core layer:
- FilingService: orchestrates batch filing with 401-aborts-batch, 409-treats-as-success, transient errors fall to DiskRetryQueue
- DiskRetryQueue: atomic write (tmp + rename), Hebrew UTF-8 round-trip, idempotent remove
- Models: MailEnvelope (STA-extracted primitives), FilingTarget, FilingOutcome enum, FilingItemResult, FilingBatchSummary, RetryQueueItem
- EspoCrmClient.CreateContactAsync for "create contact from sender" flow

UI layer:
- FileToCaseDialog (WPF, RTL, Hebrew labels) with grouped search results + recent sidebar
- FileToCaseViewModel: 250ms debounced GlobalSearch, create-contact-from-sender command, DialogResult signaling

Tests (38 passing):
- FilingServiceTests: happy path, 409, 401 aborts batch, transient enqueues, queue failure → Failed, target propagation, null guards
- DiskRetryQueueTests: id assignment, Hebrew round-trip, corrupt-file skip, idempotent remove, atomic overwrite

VSTO host project (OutlookAddin.csproj) regenerated; ThisAddIn wiring in follow-up commits.
gitignore: exclude .claude/settings.local.json (per-machine).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PointStar
2026-05-11 15:04:10 +03:00
parent f563dd17dd
commit e527aa80af
28 changed files with 1970 additions and 0 deletions
+3
View File
@@ -44,6 +44,9 @@ appsettings.local.json
*.log
logs/
# Claude Code local settings (per-machine)
.claude/settings.local.json
# OS
.DS_Store
Thumbs.db
+9
View File
@@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutlookAddin.UI", "src\Outl
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutlookAddin.Tests", "src\OutlookAddin.Tests\OutlookAddin.Tests.csproj", "{A0000000-0000-0000-0000-000000000004}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutlookAddin", "src\OutlookAddin\OutlookAddin.csproj", "{7A8B6509-CB21-42BD-A42C-2735170AFFD2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -26,8 +28,15 @@ Global
{A0000000-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0000000-0000-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0000000-0000-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU
{7A8B6509-CB21-42BD-A42C-2735170AFFD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A8B6509-CB21-42BD-A42C-2735170AFFD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A8B6509-CB21-42BD-A42C-2735170AFFD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A8B6509-CB21-42BD-A42C-2735170AFFD2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {35FDE1D2-587A-4894-8288-6A84B6BE64B9}
EndGlobalSection
EndGlobal
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
namespace MarcusLaw.OutlookAddin.Core.Models
{
public sealed class FilingBatchSummary
{
public IReadOnlyList<FilingItemResult> Items { get; set; } = new List<FilingItemResult>();
public int FiledCount => Items.Count(i => i.Outcome == FilingOutcome.Filed);
public int QueuedCount => Items.Count(i => i.Outcome == FilingOutcome.Queued);
public int FailedCount => Items.Count(i => i.Outcome == FilingOutcome.Failed);
public bool AuthRequired => Items.Any(i => i.Outcome == FilingOutcome.AuthRequired);
}
}
@@ -0,0 +1,15 @@
namespace MarcusLaw.OutlookAddin.Core.Models
{
public sealed class FilingItemResult
{
public object? Token { get; set; }
public FilingOutcome Outcome { get; set; }
public string? MessageId { get; set; }
public string? FiledEntityId { get; set; }
public string? ErrorMessage { get; set; }
}
}
@@ -0,0 +1,10 @@
namespace MarcusLaw.OutlookAddin.Core.Models
{
public enum FilingOutcome
{
Filed,
AuthRequired,
Queued,
Failed
}
}
@@ -0,0 +1,22 @@
using System;
namespace MarcusLaw.OutlookAddin.Core.Models
{
public sealed class FilingTarget
{
public FilingTarget(string parentType, string parentId, string parentName)
{
if (string.IsNullOrWhiteSpace(parentType)) throw new ArgumentException("parentType required", nameof(parentType));
if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("parentId required", nameof(parentId));
ParentType = parentType;
ParentId = parentId;
ParentName = parentName ?? string.Empty;
}
public string ParentType { get; }
public string ParentId { get; }
public string ParentName { get; }
}
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace MarcusLaw.OutlookAddin.Core.Models
{
public sealed class MailEnvelope
{
public object? Token { get; set; }
public string? MessageId { get; set; }
public string Subject { get; set; } = string.Empty;
public string From { get; set; } = string.Empty;
public List<string> To { get; set; } = new List<string>();
public List<string> Cc { get; set; } = new List<string>();
public List<string> Bcc { get; set; } = new List<string>();
public List<string> ReplyTo { get; set; } = new List<string>();
public string Body { get; set; } = string.Empty;
public bool IsHtml { get; set; }
public DateTimeOffset DateSent { get; set; }
public List<EspoAttachment> Attachments { get; set; } = new List<EspoAttachment>();
}
}
@@ -0,0 +1,26 @@
using System;
using System.Text.Json.Serialization;
namespace MarcusLaw.OutlookAddin.Core.Models
{
public sealed class RetryQueueItem
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("enqueuedAt")]
public DateTimeOffset EnqueuedAt { get; set; }
[JsonPropertyName("attemptCount")]
public int AttemptCount { get; set; }
[JsonPropertyName("lastError")]
public string? LastError { get; set; }
[JsonPropertyName("targetName")]
public string TargetName { get; set; } = string.Empty;
[JsonPropertyName("request")]
public FileEmailRequest Request { get; set; } = new FileEmailRequest();
}
}
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
using Serilog;
namespace MarcusLaw.OutlookAddin.Core.Services
{
public sealed class DiskRetryQueue : IRetryQueue
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true
};
private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(false);
private readonly string _directory;
private readonly ILogger _logger;
public DiskRetryQueue(ILogger logger)
: this(DefaultDirectory(), logger)
{
}
public DiskRetryQueue(string directory, ILogger logger)
{
if (string.IsNullOrWhiteSpace(directory)) throw new ArgumentException("directory required", nameof(directory));
_directory = directory;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public string Directory => _directory;
public Task<string> EnqueueAsync(RetryQueueItem item, CancellationToken cancellationToken = default)
{
if (item == null) throw new ArgumentNullException(nameof(item));
EnsureDirectory();
if (string.IsNullOrEmpty(item.Id))
{
item.Id = Guid.NewGuid().ToString("N");
}
if (item.EnqueuedAt == default)
{
item.EnqueuedAt = DateTimeOffset.UtcNow;
}
var finalPath = Path.Combine(_directory, item.Id + ".json");
var tempPath = finalPath + ".tmp";
var json = JsonSerializer.Serialize(item, JsonOptions);
File.WriteAllText(tempPath, json, Utf8NoBom);
if (File.Exists(finalPath))
{
File.Delete(finalPath);
}
File.Move(tempPath, finalPath);
_logger.Information("Retry queue enqueued {Id} (messageId={MessageId})", item.Id, item.Request?.MessageId);
return Task.FromResult(item.Id);
}
public Task<IReadOnlyList<RetryQueueItem>> ListAllAsync(CancellationToken cancellationToken = default)
{
var result = new List<RetryQueueItem>();
if (!System.IO.Directory.Exists(_directory))
{
return Task.FromResult<IReadOnlyList<RetryQueueItem>>(result);
}
foreach (var path in System.IO.Directory.EnumerateFiles(_directory, "*.json"))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var json = File.ReadAllText(path, Encoding.UTF8);
var item = JsonSerializer.Deserialize<RetryQueueItem>(json, JsonOptions);
if (item != null)
{
if (string.IsNullOrEmpty(item.Id))
{
item.Id = Path.GetFileNameWithoutExtension(path);
}
result.Add(item);
}
}
catch (JsonException ex)
{
_logger.Warning(ex, "Retry queue: skipping corrupt file {Path}", path);
}
catch (IOException ex)
{
_logger.Warning(ex, "Retry queue: cannot read {Path}", path);
}
}
return Task.FromResult<IReadOnlyList<RetryQueueItem>>(result);
}
public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("id required", nameof(id));
var path = Path.Combine(_directory, id + ".json");
if (File.Exists(path))
{
File.Delete(path);
_logger.Information("Retry queue removed {Id}", id);
}
return Task.CompletedTask;
}
private void EnsureDirectory()
{
if (!System.IO.Directory.Exists(_directory))
{
System.IO.Directory.CreateDirectory(_directory);
}
}
private static string DefaultDirectory()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MarcusLaw",
"OutlookAddin",
"retry-queue");
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -154,6 +155,34 @@ namespace MarcusLaw.OutlookAddin.Core.Services
public Task<UserInfo> TestConnectionAsync(CancellationToken cancellationToken = default)
=> GetJsonAsync<UserInfo>("App/user", cancellationToken);
public async Task<ContactEntity> CreateContactAsync(string name, string? emailAddress = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name required", nameof(name));
var payload = new Dictionary<string, object?>
{
["name"] = name
};
if (!string.IsNullOrWhiteSpace(emailAddress))
{
payload["emailAddress"] = emailAddress;
}
var json = JsonSerializer.Serialize(payload, JsonOptions);
var response = await ExecuteAsync(HttpMethod.Post, "Contact", json, cancellationToken).ConfigureAwait(false);
try
{
await EnsureSuccessAsync(response).ConfigureAwait(false);
var body = await ReadBodyAsync(response).ConfigureAwait(false);
return JsonSerializer.Deserialize<ContactEntity>(body, JsonOptions)
?? throw new EspoCrmHttpException(response.StatusCode, body, "Empty ContactEntity from EspoCRM");
}
finally
{
response.Dispose();
}
}
private Task<TResult> GetEntityAsync<TResult>(string entityType, string id, string? select, CancellationToken cancellationToken)
where TResult : class
{
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
using Serilog;
namespace MarcusLaw.OutlookAddin.Core.Services
{
public sealed class FilingService : IFilingService
{
private readonly IEspoCrmClient _client;
private readonly IRetryQueue _retryQueue;
private readonly ILogger _logger;
public FilingService(IEspoCrmClient client, IRetryQueue retryQueue, ILogger logger)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_retryQueue = retryQueue ?? throw new ArgumentNullException(nameof(retryQueue));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<FilingBatchSummary> FileAsync(
IReadOnlyList<MailEnvelope> envelopes,
FilingTarget target,
CancellationToken cancellationToken = default)
{
if (envelopes == null) throw new ArgumentNullException(nameof(envelopes));
if (target == null) throw new ArgumentNullException(nameof(target));
var items = new List<FilingItemResult>(envelopes.Count);
var authBlocked = false;
foreach (var env in envelopes)
{
cancellationToken.ThrowIfCancellationRequested();
if (authBlocked)
{
items.Add(new FilingItemResult
{
Token = env.Token,
MessageId = env.MessageId,
Outcome = FilingOutcome.AuthRequired,
ErrorMessage = "Skipped — earlier item rejected by EspoCRM"
});
continue;
}
var request = BuildRequest(env, target);
try
{
var response = await _client.FileMailAsync(request, cancellationToken).ConfigureAwait(false);
items.Add(new FilingItemResult
{
Token = env.Token,
MessageId = env.MessageId,
Outcome = FilingOutcome.Filed,
FiledEntityId = response?.Id
});
_logger.Information(
"Filed messageId={MessageId} to {ParentType}/{ParentId} (created={Created})",
env.MessageId, target.ParentType, target.ParentId, response?.Created);
}
catch (EspoCrmAuthorizationException ex)
{
_logger.Warning(ex, "Filing aborted: EspoCRM rejected credentials");
authBlocked = true;
items.Add(new FilingItemResult
{
Token = env.Token,
MessageId = env.MessageId,
Outcome = FilingOutcome.AuthRequired,
ErrorMessage = ex.Message
});
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Filing failed for messageId={MessageId}, enqueueing for retry", env.MessageId);
items.Add(await EnqueueOrFailAsync(env, request, target, ex, cancellationToken).ConfigureAwait(false));
}
}
return new FilingBatchSummary { Items = items };
}
private async Task<FilingItemResult> EnqueueOrFailAsync(
MailEnvelope env,
FileEmailRequest request,
FilingTarget target,
Exception primaryError,
CancellationToken cancellationToken)
{
try
{
await _retryQueue.EnqueueAsync(new RetryQueueItem
{
EnqueuedAt = DateTimeOffset.UtcNow,
Request = request,
TargetName = target.ParentName,
LastError = primaryError.Message
}, cancellationToken).ConfigureAwait(false);
return new FilingItemResult
{
Token = env.Token,
MessageId = env.MessageId,
Outcome = FilingOutcome.Queued,
ErrorMessage = primaryError.Message
};
}
catch (Exception queueEx)
{
_logger.Error(queueEx, "Retry queue enqueue failed for messageId={MessageId}", env.MessageId);
return new FilingItemResult
{
Token = env.Token,
MessageId = env.MessageId,
Outcome = FilingOutcome.Failed,
ErrorMessage = $"{primaryError.Message} | queue: {queueEx.Message}"
};
}
}
private static FileEmailRequest BuildRequest(MailEnvelope env, FilingTarget target)
{
return new FileEmailRequest
{
MessageId = env.MessageId,
Subject = env.Subject,
From = env.From,
To = new List<string>(env.To),
Cc = new List<string>(env.Cc),
Bcc = new List<string>(env.Bcc),
ReplyTo = new List<string>(env.ReplyTo),
Body = env.Body,
IsHtml = env.IsHtml,
DateSent = env.DateSent,
ParentType = target.ParentType,
ParentId = target.ParentId,
Attachments = new List<EspoAttachment>(env.Attachments)
};
}
}
}
@@ -12,6 +12,7 @@ namespace MarcusLaw.OutlookAddin.Core.Services
Task<CaseEntity> GetCaseAsync(string id, string? select = null, CancellationToken cancellationToken = default);
Task<AccountEntity> GetAccountAsync(string id, string? select = null, CancellationToken cancellationToken = default);
Task<ContactEntity> GetContactAsync(string id, string? select = null, CancellationToken cancellationToken = default);
Task<ContactEntity> CreateContactAsync(string name, string? emailAddress = null, CancellationToken cancellationToken = default);
Task<UserInfo> TestConnectionAsync(CancellationToken cancellationToken = default);
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
namespace MarcusLaw.OutlookAddin.Core.Services
{
public interface IFilingService
{
Task<FilingBatchSummary> FileAsync(
IReadOnlyList<MailEnvelope> envelopes,
FilingTarget target,
CancellationToken cancellationToken = default);
}
}
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
namespace MarcusLaw.OutlookAddin.Core.Services
{
public interface IRetryQueue
{
Task<string> EnqueueAsync(RetryQueueItem item, CancellationToken cancellationToken = default);
Task<IReadOnlyList<RetryQueueItem>> ListAllAsync(CancellationToken cancellationToken = default);
Task RemoveAsync(string id, CancellationToken cancellationToken = default);
}
}
@@ -0,0 +1,145 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Services;
using Serilog;
using Xunit;
namespace MarcusLaw.OutlookAddin.Tests.Services
{
public class DiskRetryQueueTests : IDisposable
{
private readonly string _tempDir;
private readonly ILogger _logger;
public DiskRetryQueueTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "OutlookAddinTests", Guid.NewGuid().ToString("N"));
_logger = new LoggerConfiguration().CreateLogger();
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
private DiskRetryQueue MakeQueue() => new DiskRetryQueue(_tempDir, _logger);
[Fact]
public async Task Enqueue_CreatesDirectoryAndAssignsId()
{
var queue = MakeQueue();
var id = await queue.EnqueueAsync(new RetryQueueItem
{
TargetName = "Smith Family",
Request = new FileEmailRequest { ParentType = "Case", ParentId = "case-1", Subject = "x" }
});
id.Should().NotBeNullOrEmpty();
Directory.Exists(_tempDir).Should().BeTrue();
File.Exists(Path.Combine(_tempDir, id + ".json")).Should().BeTrue();
}
[Fact]
public async Task Enqueue_StoresHebrewPayload_RoundTripsViaListAll()
{
var queue = MakeQueue();
var hebrewSubject = "תיק משפחת כהן — דיון יום ראשון";
var hebrewTarget = "תיק כהן נ' לוי";
await queue.EnqueueAsync(new RetryQueueItem
{
TargetName = hebrewTarget,
LastError = "שגיאת רשת",
Request = new FileEmailRequest
{
ParentType = "Case",
ParentId = "case-1",
Subject = hebrewSubject
}
});
var all = await queue.ListAllAsync();
all.Should().HaveCount(1);
all[0].TargetName.Should().Be(hebrewTarget);
all[0].LastError.Should().Be("שגיאת רשת");
all[0].Request.Subject.Should().Be(hebrewSubject);
}
[Fact]
public async Task ListAll_ReturnsEmptyWhenDirectoryMissing()
{
var queue = MakeQueue();
var all = await queue.ListAllAsync();
all.Should().BeEmpty();
}
[Fact]
public async Task ListAll_SkipsCorruptFiles()
{
Directory.CreateDirectory(_tempDir);
File.WriteAllText(Path.Combine(_tempDir, "corrupt.json"), "{not valid json");
var queue = MakeQueue();
await queue.EnqueueAsync(new RetryQueueItem
{
Request = new FileEmailRequest { ParentType = "Case", ParentId = "case-1" }
});
var all = await queue.ListAllAsync();
all.Should().HaveCount(1);
}
[Fact]
public async Task Remove_DeletesFile()
{
var queue = MakeQueue();
var id = await queue.EnqueueAsync(new RetryQueueItem
{
Request = new FileEmailRequest { ParentType = "Case", ParentId = "case-1" }
});
await queue.RemoveAsync(id);
File.Exists(Path.Combine(_tempDir, id + ".json")).Should().BeFalse();
(await queue.ListAllAsync()).Should().BeEmpty();
}
[Fact]
public async Task Remove_IsIdempotent()
{
var queue = MakeQueue();
await queue.RemoveAsync("does-not-exist");
(await queue.ListAllAsync()).Should().BeEmpty();
}
[Fact]
public async Task Enqueue_OverwritesExistingIdAtomically()
{
var queue = MakeQueue();
var item = new RetryQueueItem
{
Id = "fixed-id",
Request = new FileEmailRequest { ParentType = "Case", ParentId = "case-1", Subject = "v1" }
};
await queue.EnqueueAsync(item);
item.Request.Subject = "v2";
item.AttemptCount = 1;
await queue.EnqueueAsync(item);
var all = await queue.ListAllAsync();
all.Should().HaveCount(1);
all[0].Request.Subject.Should().Be("v2");
all[0].AttemptCount.Should().Be(1);
Directory.EnumerateFiles(_tempDir, "*.tmp").Should().BeEmpty();
}
}
}
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Services;
using Moq;
using Serilog;
using Xunit;
namespace MarcusLaw.OutlookAddin.Tests.Services
{
public class FilingServiceTests
{
private static readonly ILogger Logger = new LoggerConfiguration().CreateLogger();
private static MailEnvelope MakeEnvelope(string token, string? messageId = null) => new MailEnvelope
{
Token = token,
MessageId = messageId ?? "<" + token + "@server>",
Subject = "RE: " + token,
From = "lawyer@marcus-law.co.il",
To = new List<string> { "client@example.com" },
Body = "body of " + token,
DateSent = DateTimeOffset.UtcNow
};
private static FilingTarget Target => new FilingTarget("Case", "case-123", "Smith v Jones");
[Fact]
public async Task FileAsync_HappyPath_AllItemsFiled()
{
var client = new Mock<IEspoCrmClient>(MockBehavior.Strict);
client.Setup(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((FileEmailRequest req, CancellationToken _) => new FileEmailResponse
{
Id = "email-" + req.MessageId,
Created = true,
ParentType = req.ParentType,
ParentId = req.ParentId,
MessageId = req.MessageId
});
var queue = new Mock<IRetryQueue>(MockBehavior.Strict);
var svc = new FilingService(client.Object, queue.Object, Logger);
var envelopes = new[] { MakeEnvelope("A"), MakeEnvelope("B") };
var summary = await svc.FileAsync(envelopes, Target);
summary.Items.Should().HaveCount(2);
summary.FiledCount.Should().Be(2);
summary.QueuedCount.Should().Be(0);
summary.AuthRequired.Should().BeFalse();
summary.Items[0].FiledEntityId.Should().Be("email-<A@server>");
summary.Items[0].Token.Should().Be("A");
}
[Fact]
public async Task FileAsync_409ConflictIsStillSuccess()
{
var client = new Mock<IEspoCrmClient>(MockBehavior.Strict);
client.Setup(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new FileEmailResponse { Id = "existing", Created = false });
var queue = new Mock<IRetryQueue>(MockBehavior.Strict);
var svc = new FilingService(client.Object, queue.Object, Logger);
var summary = await svc.FileAsync(new[] { MakeEnvelope("A") }, Target);
summary.FiledCount.Should().Be(1);
summary.Items[0].Outcome.Should().Be(FilingOutcome.Filed);
summary.Items[0].FiledEntityId.Should().Be("existing");
}
[Fact]
public async Task FileAsync_401_SetsAuthRequiredAndSkipsRemaining()
{
var client = new Mock<IEspoCrmClient>(MockBehavior.Strict);
client.Setup(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new EspoCrmAuthorizationException("API key rejected"));
var queue = new Mock<IRetryQueue>(MockBehavior.Strict);
var svc = new FilingService(client.Object, queue.Object, Logger);
var summary = await svc.FileAsync(new[] { MakeEnvelope("A"), MakeEnvelope("B") }, Target);
summary.AuthRequired.Should().BeTrue();
summary.FiledCount.Should().Be(0);
summary.Items.Should().AllSatisfy(i => i.Outcome.Should().Be(FilingOutcome.AuthRequired));
client.Verify(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task FileAsync_TransientFailure_EnqueuesToRetryQueue()
{
var client = new Mock<IEspoCrmClient>(MockBehavior.Strict);
client.Setup(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new System.Net.Http.HttpRequestException("connection reset"));
var enqueued = new List<RetryQueueItem>();
var queue = new Mock<IRetryQueue>(MockBehavior.Strict);
queue.Setup(q => q.EnqueueAsync(It.IsAny<RetryQueueItem>(), It.IsAny<CancellationToken>()))
.Callback<RetryQueueItem, CancellationToken>((it, _) => enqueued.Add(it))
.ReturnsAsync("queue-id");
var svc = new FilingService(client.Object, queue.Object, Logger);
var summary = await svc.FileAsync(new[] { MakeEnvelope("A") }, Target);
summary.FiledCount.Should().Be(0);
summary.QueuedCount.Should().Be(1);
summary.Items[0].Outcome.Should().Be(FilingOutcome.Queued);
enqueued.Should().HaveCount(1);
enqueued[0].Request.ParentType.Should().Be("Case");
enqueued[0].Request.ParentId.Should().Be("case-123");
enqueued[0].TargetName.Should().Be("Smith v Jones");
}
[Fact]
public async Task FileAsync_QueueFailureFallsThroughToFailed()
{
var client = new Mock<IEspoCrmClient>(MockBehavior.Strict);
client.Setup(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("primary"));
var queue = new Mock<IRetryQueue>(MockBehavior.Strict);
queue.Setup(q => q.EnqueueAsync(It.IsAny<RetryQueueItem>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new System.IO.IOException("disk full"));
var svc = new FilingService(client.Object, queue.Object, Logger);
var summary = await svc.FileAsync(new[] { MakeEnvelope("A") }, Target);
summary.FailedCount.Should().Be(1);
summary.Items[0].Outcome.Should().Be(FilingOutcome.Failed);
summary.Items[0].ErrorMessage.Should().Contain("primary").And.Contain("disk full");
}
[Fact]
public async Task FileAsync_PassesParentTargetIntoRequest()
{
FileEmailRequest? captured = null;
var client = new Mock<IEspoCrmClient>();
client.Setup(c => c.FileMailAsync(It.IsAny<FileEmailRequest>(), It.IsAny<CancellationToken>()))
.Callback<FileEmailRequest, CancellationToken>((req, _) => captured = req)
.ReturnsAsync(new FileEmailResponse { Id = "x", Created = true });
var queue = new Mock<IRetryQueue>();
var svc = new FilingService(client.Object, queue.Object, Logger);
await svc.FileAsync(new[] { MakeEnvelope("A") }, Target);
captured.Should().NotBeNull();
captured!.ParentType.Should().Be("Case");
captured.ParentId.Should().Be("case-123");
captured.MessageId.Should().Be("<A@server>");
}
[Fact]
public async Task FileAsync_RejectsNullArguments()
{
var svc = new FilingService(Mock.Of<IEspoCrmClient>(), Mock.Of<IRetryQueue>(), Logger);
await Assert.ThrowsAsync<ArgumentNullException>(() => svc.FileAsync(null!, Target));
await Assert.ThrowsAsync<ArgumentNullException>(() => svc.FileAsync(Array.Empty<MailEnvelope>(), null!));
}
}
}
@@ -0,0 +1,102 @@
<Window x:Class="MarcusLaw.OutlookAddin.UI.Dialogs.FileToCaseDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="תיוק מייל ל-EspoCRM"
Height="500" Width="600"
MinHeight="400" MinWidth="500"
FlowDirection="RightToLeft"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
ResizeMode="CanResize">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,8" LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Text="חיפוש:" VerticalAlignment="Center" Margin="0,0,8,0" />
<TextBox x:Name="SearchBox"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Padding="6,4"
FontSize="14" />
</DockPanel>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="170" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" BorderBrush="#CCC" BorderThickness="1" CornerRadius="3">
<ListBox ItemsSource="{Binding ResultsView}"
SelectedItem="{Binding SelectedTarget}"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Border Background="#F2F2F2" Padding="6,3">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Padding="2,3" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<DockPanel Grid.Column="1" Margin="8,0,0,0" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" Text="אחרונים" FontWeight="Bold" Margin="0,0,0,4" />
<Border BorderBrush="#CCC" BorderThickness="1" CornerRadius="3">
<ListBox ItemsSource="{Binding RecentEntities}"
SelectedItem="{Binding SelectedTarget}"
BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,2">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding EntityType}" FontSize="10" Foreground="Gray" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</DockPanel>
</Grid>
<DockPanel Grid.Row="2" Margin="0,8,0,8" LastChildFill="True">
<Button DockPanel.Dock="Right"
Command="{Binding CreateContactFromSenderCommand}"
Content="צור איש קשר חדש מהשולח"
Padding="10,4" />
<TextBlock Text="{Binding StatusMessage}"
VerticalAlignment="Center"
Margin="8,0"
Foreground="Gray"
TextTrimming="CharacterEllipsis" />
</DockPanel>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Command="{Binding SubmitCommand}"
IsDefault="True"
Content="תייק"
Padding="20,5"
Margin="0,0,8,0"
MinWidth="100" />
<Button Command="{Binding CancelCommand}"
IsCancel="True"
Content="ביטול"
Padding="20,5"
MinWidth="100" />
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,44 @@
using System;
using System.Windows;
using MarcusLaw.OutlookAddin.UI.ViewModels;
namespace MarcusLaw.OutlookAddin.UI.Dialogs
{
public partial class FileToCaseDialog : Window
{
private FileToCaseViewModel? _viewModel;
public FileToCaseDialog()
{
InitializeComponent();
Loaded += OnLoaded;
}
public FileToCaseDialog(FileToCaseViewModel viewModel) : this()
{
AttachViewModel(viewModel);
}
public void AttachViewModel(FileToCaseViewModel viewModel)
{
if (_viewModel != null)
{
_viewModel.RequestClose -= OnRequestClose;
}
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
DataContext = _viewModel;
_viewModel.RequestClose += OnRequestClose;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
SearchBox.Focus();
}
private void OnRequestClose(object? sender, EventArgs e)
{
DialogResult = _viewModel?.DialogResult;
Close();
}
}
}
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MarcusLaw.OutlookAddin.Core.Configuration;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Services;
using Serilog;
namespace MarcusLaw.OutlookAddin.UI.ViewModels
{
public sealed partial class FileToCaseViewModel : ObservableObject
{
private readonly IEspoCrmClient _client;
private readonly ILogger _logger;
private CancellationTokenSource? _searchCts;
[ObservableProperty]
private string searchText = string.Empty;
[ObservableProperty]
private bool isBusy;
[ObservableProperty]
private EspoEntityRef? selectedTarget;
[ObservableProperty]
private string? senderEmail;
[ObservableProperty]
private string? senderName;
[ObservableProperty]
private string? statusMessage;
public ObservableCollection<EspoEntityRef> Results { get; } = new ObservableCollection<EspoEntityRef>();
public ObservableCollection<EspoEntityRef> RecentEntities { get; } = new ObservableCollection<EspoEntityRef>();
public ICollectionView ResultsView { get; }
public bool? DialogResult { get; private set; }
public event EventHandler? RequestClose;
public FileToCaseViewModel(IEspoCrmClient client, AppSettings settings, ILogger logger)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
if (settings == null) throw new ArgumentNullException(nameof(settings));
foreach (var r in settings.RecentEntities)
{
RecentEntities.Add(r);
}
ResultsView = CollectionViewSource.GetDefaultView(Results);
ResultsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(EspoEntityRef.EntityType)));
}
partial void OnSearchTextChanged(string value)
{
_searchCts?.Cancel();
_searchCts = new CancellationTokenSource();
_ = SearchAsync(value, _searchCts.Token);
}
partial void OnSelectedTargetChanged(EspoEntityRef? value)
{
SubmitCommand.NotifyCanExecuteChanged();
}
partial void OnSenderEmailChanged(string? value)
{
CreateContactFromSenderCommand.NotifyCanExecuteChanged();
}
private async Task SearchAsync(string query, CancellationToken ct)
{
try
{
await Task.Delay(250, ct);
var trimmed = (query ?? string.Empty).Trim();
if (trimmed.Length < 2)
{
Results.Clear();
StatusMessage = null;
return;
}
IsBusy = true;
var result = await _client.GlobalSearchAsync(trimmed, ct);
ct.ThrowIfCancellationRequested();
Results.Clear();
foreach (var r in result.List)
{
Results.Add(r);
}
StatusMessage = $"נמצאו {result.Total} תוצאות";
}
catch (OperationCanceledException)
{
}
catch (EspoCrmAuthorizationException)
{
StatusMessage = "מפתח ה-API נדחה — יש להזין מחדש";
}
catch (Exception ex)
{
_logger.Warning(ex, "GlobalSearch failed");
StatusMessage = "שגיאת חיפוש: " + ex.Message;
}
finally
{
IsBusy = false;
}
}
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
DialogResult = true;
RequestClose?.Invoke(this, EventArgs.Empty);
}
private bool CanSubmit() => SelectedTarget != null && !string.IsNullOrWhiteSpace(SelectedTarget.Id);
[RelayCommand]
private void Cancel()
{
DialogResult = false;
RequestClose?.Invoke(this, EventArgs.Empty);
}
[RelayCommand(CanExecute = nameof(CanCreateContact))]
private async Task CreateContactFromSenderAsync()
{
if (string.IsNullOrWhiteSpace(SenderEmail)) return;
try
{
IsBusy = true;
var name = !string.IsNullOrWhiteSpace(SenderName) ? SenderName! : SenderEmail!;
var contact = await _client.CreateContactAsync(name, SenderEmail);
var entry = new EspoEntityRef
{
Id = contact.Id,
Name = contact.Name ?? name,
EntityType = "Contact"
};
Results.Insert(0, entry);
SelectedTarget = entry;
StatusMessage = "איש קשר נוצר: " + entry.Name;
}
catch (EspoCrmAuthorizationException)
{
StatusMessage = "מפתח ה-API נדחה — יש להזין מחדש";
}
catch (Exception ex)
{
_logger.Error(ex, "CreateContact failed");
StatusMessage = "יצירת איש קשר נכשלה: " + ex.Message;
}
finally
{
IsBusy = false;
}
}
private bool CanCreateContact() => !string.IsNullOrWhiteSpace(SenderEmail);
public FilingTarget? GetSelectedFilingTarget()
{
if (SelectedTarget == null || string.IsNullOrWhiteSpace(SelectedTarget.Id) ||
string.IsNullOrWhiteSpace(SelectedTarget.EntityType))
{
return null;
}
return new FilingTarget(SelectedTarget.EntityType, SelectedTarget.Id, SelectedTarget.Name ?? string.Empty);
}
}
}
+264
View File
@@ -0,0 +1,264 @@
<Project ToolsVersion="17.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<!--
This section defines project-level properties.
AssemblyName
Name of the output assembly.
Configuration
Specifies a default value for debug.
OutputType
Must be "Library" for VSTO.
Platform
Specifies what CPU the output of this project can run on.
NoStandardLibraries
Set to "false" for VSTO.
RootNamespace
In C#, this specifies the namespace given to new files. In VB, all objects are
wrapped in this namespace at runtime.
-->
<PropertyGroup>
<ProjectTypeGuids>{BAA0C2D2-18E2-41B9-852F-F413020CAA33};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7A8B6509-CB21-42BD-A42C-2735170AFFD2}</ProjectGuid>
<OutputType>Library</OutputType>
<NoStandardLibraries>false</NoStandardLibraries>
<RootNamespace>OutlookAddin</RootNamespace>
<AssemblyName>MarcusLaw.OutlookAddin</AssemblyName>
<LoadBehavior>3</LoadBehavior>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<DefineConstants>VSTO40;UseOfficeInterop</DefineConstants>
<BootstrapperEnabled>true</BootstrapperEnabled>
<BootstrapperComponentsLocation>HomeSite</BootstrapperComponentsLocation>
<ResolveComReferenceSilent>true</ResolveComReferenceSilent>
</PropertyGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.VSTORuntime.4.0">
<Visible>False</Visible>
<ProductName>Microsoft Visual Studio 2010 Tools for Office Runtime %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<PropertyGroup>
<!--
OfficeApplication
Add-in host application
-->
<OfficeApplication>Outlook</OfficeApplication>
</PropertyGroup>
<!--
This section defines properties that are set when the "Debug" configuration is selected.
DebugSymbols
If "true", create symbols (.pdb). If "false", do not create symbols.
DefineConstants
Constants defined for the preprocessor.
EnableUnmanagedDebugging
If "true", starting the debugger will attach both managed and unmanaged debuggers.
Optimize
If "true", optimize the build output. If "false", do not optimize.
OutputPath
Output path of project relative to the project file.
WarningLevel
Warning level for the compiler.
-->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<!--
This section defines properties that are set when the "Release" configuration is selected.
DebugSymbols
If "true", create symbols (.pdb). If "false", do not create symbols.
DefineConstants
Constants defined for the preprocessor.
EnableUnmanagedDebugging
If "true", starting the debugger will attach both managed and unmanaged debuggers.
Optimize
If "true", optimize the build output. If "false", do not optimize.
OutputPath
Output path of project relative to the project file.
WarningLevel
Warning level for the compiler.
-->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<DefineConstants>$(DefineConstants);TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<!--
This section specifies references for the project.
-->
<ItemGroup>
<Reference Include="Accessibility" />
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Office.Tools.v4.0.Framework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.Tools.Applications.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Office.Tools, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Office.Tools.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Office.Tools.Outlook, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Office.Tools.Common.v4.0.Utilities, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Office.Tools.Outlook.v4.0.Utilities, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>True</Private>
</Reference>
</ItemGroup>
<Choose>
<When Condition="$([System.String]::Copy(&quot;;$(DefineConstants);&quot;).ToLower().Contains(';useofficeinterop;')) or $([System.String]::Copy(&quot;,$(DefineConstants),&quot;).ToLower().Contains(',useofficeinterop,'))">
<ItemGroup>
<Reference Include="Office, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
<Private>False</Private>
<EmbedInteropTypes>true</EmbedInteropTypes>
</Reference>
<Reference Include="Microsoft.Office.Interop.Outlook, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
<Private>False</Private>
<EmbedInteropTypes>true</EmbedInteropTypes>
</Reference>
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<COMReference Include="Microsoft.Office.Core">
<Guid>{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}</Guid>
<VersionMajor>2</VersionMajor>
<VersionMinor>7</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
<COMReference Include="Microsoft.Office.Interop.Outlook">
<Guid>{00062FFF-0000-0000-C000-000000000046}</Guid>
<VersionMajor>9</VersionMajor>
<VersionMinor>5</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<Reference Include="stdole, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<Private>False</Private>
</Reference>
</ItemGroup>
<!--
This section defines the user source files that are part of the project.
A "Compile" element specifies a source file to compile.
An "EmbeddedResource" element specifies an .resx file for embedded resources.
A "None" element specifies a file that is not to be passed to the compiler (for instance,
a text file or XML file).
The "AppDesigner" element specifies the directory where the application properties files
can be found.
-->
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="OutlookAddin_TemporaryKey.pfx" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<Compile Include="ThisAddIn.cs">
<SubType>Code</SubType>
</Compile>
<None Include="ThisAddIn.Designer.xml">
<DependentUpon>ThisAddIn.cs</DependentUpon>
</None>
<Compile Include="ThisAddIn.Designer.cs">
<DependentUpon>ThisAddIn.Designer.xml</DependentUpon>
</Compile>
<AppDesigner Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OutlookAddin.Core\OutlookAddin.Core.csproj">
<Project>{a0000000-0000-0000-0000-000000000002}</Project>
<Name>OutlookAddin.Core</Name>
</ProjectReference>
<ProjectReference Include="..\OutlookAddin.UI\OutlookAddin.UI.csproj">
<Project>{a0000000-0000-0000-0000-000000000003}</Project>
<Name>OutlookAddin.UI</Name>
</ProjectReference>
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<PropertyGroup>
<AutoGenerateBindingRedirects>false</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup>
<SignManifests>true</SignManifests>
</PropertyGroup>
<PropertyGroup>
<ManifestKeyFile>OutlookAddin_TemporaryKey.pfx</ManifestKeyFile>
</PropertyGroup>
<PropertyGroup>
<ManifestCertificateThumbprint>2399E9BF73820F3B1FC744BCBFA7F82D536E3256</ManifestCertificateThumbprint>
</PropertyGroup>
<!-- Include the build rules for a C# project. -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- Include additional build rules for an Office application add-in. -->
<Import Project="$(VSToolsPath)\OfficeTools\Microsoft.VisualStudio.Tools.Office.targets" Condition="'$(VSToolsPath)' != ''" />
<!-- This section defines VSTO properties that describe the host-changeable project properties. -->
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{BAA0C2D2-18E2-41B9-852F-F413020CAA33}">
<ProjectProperties HostName="Outlook" HostPackage="{29A7B9D7-A7F1-4328-8EF0-6B2D1A56B2C1}" OfficeVersion="15.0" VstxVersion="4.0" ApplicationType="Outlook" Language="cs" TemplatesPath="" DebugInfoExeName="#Software\Microsoft\Office\16.0\Outlook\InstallRoot\Path#outlook.exe" AddItemTemplatesGuid="{A58A78EB-1C92-4DDD-80CF-E8BD872ABFC4}" />
<Host Name="Outlook" GeneratedCodeNamespace="OutlookAddin" IconIndex="0">
<HostItem Name="ThisAddIn" Code="ThisAddIn.cs" CanonicalName="AddIn" CanActivate="false" IconIndex="1" Blueprint="ThisAddIn.Designer.xml" GeneratedCode="ThisAddIn.Designer.cs" />
</Host>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>
@@ -0,0 +1,38 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MarcusLaw.OutlookAddin")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MarcusLaw.OutlookAddin")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("a67378ed-b53f-4674-a713-520408025b9d")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
+62
View File
@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace OutlookAddin.Properties {
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OutlookAddin.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}
+117
View File
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+26
View File
@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace OutlookAddin.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>
+286
View File
@@ -0,0 +1,286 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#pragma warning disable 414
namespace OutlookAddin {
///
[Microsoft.VisualStudio.Tools.Applications.Runtime.StartupObjectAttribute(0)]
[global::System.Security.Permissions.PermissionSetAttribute(global::System.Security.Permissions.SecurityAction.Demand, Name="FullTrust")]
public sealed partial class ThisAddIn : Microsoft.Office.Tools.Outlook.OutlookAddInBase {
internal Microsoft.Office.Tools.CustomTaskPaneCollection CustomTaskPanes;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
private global::System.Object missing = global::System.Type.Missing;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
internal Microsoft.Office.Interop.Outlook.Application Application;
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
public ThisAddIn(global::Microsoft.Office.Tools.Outlook.Factory factory, global::System.IServiceProvider serviceProvider) :
base(factory, serviceProvider, "AddIn", "ThisAddIn") {
Globals.Factory = factory;
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void Initialize() {
base.Initialize();
this.Application = this.GetHostItem<Microsoft.Office.Interop.Outlook.Application>(typeof(Microsoft.Office.Interop.Outlook.Application), "Application");
Globals.ThisAddIn = this;
global::System.Windows.Forms.Application.EnableVisualStyles();
this.InitializeCachedData();
this.InitializeControls();
this.InitializeComponents();
this.InitializeData();
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void FinishInitialization() {
this.InternalStartup();
this.OnStartup();
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void InitializeDataBindings() {
this.BeginInitialization();
this.BindToData();
this.EndInitialization();
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void InitializeCachedData() {
if ((this.DataHost == null)) {
return;
}
if (this.DataHost.IsCacheInitialized) {
this.DataHost.FillCachedData(this);
}
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void InitializeData() {
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void BindToData() {
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
private void StartCaching(string MemberName) {
this.DataHost.StartCaching(this, MemberName);
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
private void StopCaching(string MemberName) {
this.DataHost.StopCaching(this, MemberName);
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
private bool IsCached(string MemberName) {
return this.DataHost.IsCached(this, MemberName);
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void BeginInitialization() {
this.BeginInit();
this.CustomTaskPanes.BeginInit();
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void EndInitialization() {
this.CustomTaskPanes.EndInit();
this.EndInit();
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void InitializeControls() {
this.CustomTaskPanes = Globals.Factory.CreateCustomTaskPaneCollection(null, null, "CustomTaskPanes", "CustomTaskPanes", this);
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
private void InitializeComponents() {
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
private bool NeedsFill(string MemberName) {
return this.DataHost.NeedsFill(this, MemberName);
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void OnShutdown() {
this.CustomTaskPanes.Dispose();
base.OnShutdown();
}
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
internal sealed partial class Globals {
///
private Globals() {
}
private static ThisAddIn _ThisAddIn;
private static global::Microsoft.Office.Tools.Outlook.Factory _factory;
private static ThisRibbonCollection _ThisRibbonCollection;
private static ThisFormRegionCollection _ThisFormRegionCollection;
internal static ThisAddIn ThisAddIn {
get {
return _ThisAddIn;
}
set {
if ((_ThisAddIn == null)) {
_ThisAddIn = value;
}
else {
throw new System.NotSupportedException();
}
}
}
internal static global::Microsoft.Office.Tools.Outlook.Factory Factory {
get {
return _factory;
}
set {
if ((_factory == null)) {
_factory = value;
}
else {
throw new System.NotSupportedException();
}
}
}
internal static ThisRibbonCollection Ribbons {
get {
if ((_ThisRibbonCollection == null)) {
_ThisRibbonCollection = new ThisRibbonCollection(_factory.GetRibbonFactory());
}
return _ThisRibbonCollection;
}
}
internal static ThisFormRegionCollection FormRegions {
get {
if ((_ThisFormRegionCollection == null)) {
_ThisFormRegionCollection = new ThisFormRegionCollection(Globals.ThisAddIn.GetFormRegions());
}
return _ThisFormRegionCollection;
}
}
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Office.ProgrammingModel.dll", "17.0.0.0")]
internal sealed partial class ThisRibbonCollection : Microsoft.Office.Tools.Ribbon.RibbonCollectionBase {
///
internal ThisRibbonCollection(global::Microsoft.Office.Tools.Ribbon.RibbonFactory factory) :
base(factory) {
}
internal ThisRibbonCollection this[Microsoft.Office.Interop.Outlook.Inspector inspector] {
get {
return this.GetRibbonContextCollection<ThisRibbonCollection>(inspector);
}
}
internal ThisRibbonCollection this[Microsoft.Office.Interop.Outlook.Explorer explorer] {
get {
return this.GetRibbonContextCollection<ThisRibbonCollection>(explorer);
}
}
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
internal sealed partial class ThisFormRegionCollection : Microsoft.Office.Tools.Outlook.FormRegionCollectionBase {
///
public ThisFormRegionCollection(System.Collections.Generic.IList<Microsoft.Office.Tools.Outlook.IFormRegion> list) :
base(list) {
}
internal WindowFormRegionCollection this[Microsoft.Office.Interop.Outlook.Explorer explorer] {
get {
return ((WindowFormRegionCollection)(Globals.ThisAddIn.GetFormRegions(explorer, typeof(WindowFormRegionCollection))));
}
}
internal WindowFormRegionCollection this[Microsoft.Office.Interop.Outlook.Inspector inspector] {
get {
return ((WindowFormRegionCollection)(Globals.ThisAddIn.GetFormRegions(inspector, typeof(WindowFormRegionCollection))));
}
}
}
///
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
internal sealed partial class WindowFormRegionCollection : Microsoft.Office.Tools.Outlook.FormRegionCollectionBase {
///
public WindowFormRegionCollection(System.Collections.Generic.IList<Microsoft.Office.Tools.Outlook.IFormRegion> list) :
base(list) {
}
}
}
+4
View File
@@ -0,0 +1,4 @@
<hostitem:hostItem hostitem:baseType="Microsoft.Office.Tools.Outlook.OutlookAddInBase" hostitem:namespace="OutlookAddin" hostitem:className="ThisAddIn" hostitem:identifier="ThisAddIn" hostitem:primaryCookie="AddIn" hostitem:master="true" hostitem:factoryType="Microsoft.Office.Tools.Outlook.Factory" hostitem:startupIndex="0" xmlns:hostitem="http://schemas.microsoft.com/2004/VisualStudio/Tools/Applications/HostItem.xsd">
<hostitem:hostObject hostitem:name="Application" hostitem:identifier="Application" hostitem:type="Microsoft.Office.Interop.Outlook.Application" hostitem:cookie="Application" hostitem:modifier="Internal" />
<hostitem:hostControl hostitem:name="CustomTaskPanes" hostitem:identifier="CustomTaskPanes" hostitem:type="Microsoft.Office.Tools.CustomTaskPaneCollection" hostitem:primaryCookie="CustomTaskPanes" hostitem:modifier="Internal" />
</hostitem:hostItem>
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Outlook = Microsoft.Office.Interop.Outlook;
using Office = Microsoft.Office.Core;
namespace OutlookAddin
{
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
// Note: Outlook no longer raises this event. If you have code that
// must run when Outlook shuts down, see https://go.microsoft.com/fwlink/?LinkId=506785
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}