Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18b6640e57 | |||
| dbd7f3068b | |||
| b6624a924f | |||
| e3fb0e95d1 | |||
| 9e7a64045f | |||
| 017f10e60b | |||
| be5edc203c | |||
| 1788b5bbee | |||
| 8011d4e34c | |||
| 5242abb585 | |||
| 037d573e6e | |||
| cb7d530b4c | |||
| 7924cda520 | |||
| 7ee9a00fc4 | |||
| af8cf13443 | |||
| 26322c92ff | |||
| c98fee7b5b | |||
| 1a9ca2d2ad | |||
| b1f8e3e50b | |||
| 0fea393b81 |
@@ -0,0 +1,15 @@
|
||||
* text=auto
|
||||
|
||||
# ClickOnce publish output must be byte-identical to the hashes recorded
|
||||
# in dll.manifest at publish time. Treat everything under Publish/ as
|
||||
# binary so git's CRLF/LF normalization can't change the bytes between
|
||||
# what VS published and what end-users download via setup.exe.
|
||||
Publish/** -text
|
||||
|
||||
# Defense in depth — apply the same rule to ClickOnce extensions
|
||||
# regardless of where they live.
|
||||
*.deploy binary
|
||||
*.manifest binary
|
||||
*.vsto binary
|
||||
*.application binary
|
||||
*.pfx binary
|
||||
+8
-2
@@ -24,16 +24,22 @@ project.lock.json
|
||||
project.fragment.lock.json
|
||||
|
||||
# Publish output (ClickOnce / MSIX)
|
||||
publish/
|
||||
# NOTE: Publish/ itself is COMMITTED so end-users can fetch the ClickOnce
|
||||
# bundle via Gitea raw URLs (https://gitea.dev.marcus-law.co.il/.../raw/
|
||||
# branch/main/Publish/). VS dumps build output there on every Publish.
|
||||
PublishProfiles/
|
||||
*.pubxml.user
|
||||
|
||||
# VSTO build output
|
||||
# VSTO build output — these wildcards exclude intermediate bin/obj
|
||||
# artifacts. Re-include everything under Publish/ which is the
|
||||
# ClickOnce distribution root (must contain .vsto/.application/.deploy
|
||||
# /.manifest files for the installer to work).
|
||||
*_TemporaryKey.pfx
|
||||
*.vsto
|
||||
*.application
|
||||
*.deploy
|
||||
*.manifest
|
||||
!Publish/**
|
||||
|
||||
# Code signing — NEVER commit private keys
|
||||
*.pfx
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+35
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.2" newVersion="8.0.0.2" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.ComponentModel.Annotations" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.2.1.0" newVersion="4.2.1.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
BIN
Binary file not shown.
+588
@@ -0,0 +1,588 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2">
|
||||
<asmv1:assemblyIdentity name="MarcusLaw.OutlookAddin.dll" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" type="win32" />
|
||||
<description xmlns="urn:schemas-microsoft-com:asm.v1">MarcusLaw.OutlookAddin</description>
|
||||
<application />
|
||||
<entryPoint>
|
||||
<co.v1:customHostSpecified />
|
||||
</entryPoint>
|
||||
<trustInfo>
|
||||
<security>
|
||||
<applicationRequestMinimum>
|
||||
<PermissionSet Unrestricted="true" ID="Custom" SameSite="site" />
|
||||
<defaultAssemblyRequest permissionSetReference="Custom" />
|
||||
</applicationRequestMinimum>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!--
|
||||
UAC Manifest Options
|
||||
If you want to change the Windows User Account Control level replace the
|
||||
requestedExecutionLevel node with one of the following.
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
If you want to utilize File and Registry Virtualization for backward
|
||||
compatibility then delete the requestedExecutionLevel node.
|
||||
-->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<dependency>
|
||||
<dependentOS>
|
||||
<osVersionInfo>
|
||||
<os majorVersion="5" minorVersion="1" buildNumber="2600" servicePackMajor="0" />
|
||||
</osVersionInfo>
|
||||
</dependentOS>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.Windows.CommonLanguageRuntime" version="4.0.30319.0" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Accessibility" version="4.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.CSharp" version="4.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.Office.Tools" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.Office.Tools.Common" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.Office.Tools.Outlook" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.Office.Tools.v4.0.Framework" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="Microsoft.VisualStudio.Tools.Applications.Runtime" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="mscorlib" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="netstandard" version="2.0.0.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="PresentationCore" version="4.0.0.0" publicKeyToken="31BF3856AD364E35" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="PresentationFramework" version="4.0.0.0" publicKeyToken="31BF3856AD364E35" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="stdole" version="7.0.3300.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.ComponentModel.DataAnnotations" version="4.0.0.0" publicKeyToken="31BF3856AD364E35" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Core" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Data" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Data.DataSetExtensions" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Deployment" version="4.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Drawing" version="4.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Net.Http" version="4.2.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Numerics" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Security" version="4.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Windows.Forms" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Xaml" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Xml" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="System.Xml.Linq" version="4.0.0.0" publicKeyToken="B77A5C561934E089" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="WindowsBase" version="4.0.0.0" publicKeyToken="31BF3856AD364E35" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
|
||||
<assemblyIdentity name="WindowsFormsIntegration" version="4.0.0.0" publicKeyToken="31BF3856AD364E35" language="neutral" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="CommunityToolkit.Mvvm.dll" size="115496">
|
||||
<assemblyIdentity name="CommunityToolkit.Mvvm" version="8.3.0.0" publicKeyToken="4AFF67A105548EE2" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>6Jm7PxxMSE5OkBu7iJrppKUcBydJn1dv4z/0D/2bhio=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="MarcusLaw.OutlookAddin.dll" size="104960">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin" version="1.0.7.0" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>UFDuAUnK7LIuno/5e6mwDuBGdKJjD6uv4mhSODRsaLI=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="MarcusLaw.OutlookAddin.Core.dll" size="94208">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.Core" version="1.0.0.0" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>AycV+vLsh7n9HMhrJdeglo3VQIc0GxGLYzffJuzQEKI=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="MarcusLaw.OutlookAddin.UI.dll" size="98816">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.UI" version="1.0.0.0" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>QrrK6ct5D6PUeQW7tkvGmMXXafUw1y/cYMzlf05OkWk=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="en-US\MarcusLaw.OutlookAddin.UI.resources.dll" size="7168">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.UI.resources" version="1.0.0.0" language="en-US" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>bwKiIldXtJujP3A/5q5LqYQmTcq5rk3P1IKlRHPF5ks=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Bcl.AsyncInterfaces.dll" size="26904">
|
||||
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" version="8.0.0.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>gGeCA70CA6ZZT04zCyJUPA3lBZOCuxyTNLeGi48xsbw=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Bcl.TimeProvider.dll" size="32528">
|
||||
<assemblyIdentity name="Microsoft.Bcl.TimeProvider" version="8.0.0.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>aTJCtn2pGveN2kuR1gIOC4zaCNTJECF3oSUiAJ75uUA=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Extensions.Caching.Abstractions.dll" size="34464">
|
||||
<assemblyIdentity name="Microsoft.Extensions.Caching.Abstractions" version="8.0.0.0" publicKeyToken="ADB9793829DDAE60" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>9SqzavfWhbalT0vUMnzHXHpjwQOdET0vxf4gYix1WxM=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Extensions.Caching.Memory.dll" size="50984">
|
||||
<assemblyIdentity name="Microsoft.Extensions.Caching.Memory" version="8.0.0.1" publicKeyToken="ADB9793829DDAE60" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>wv358DZn4LYfOU9AvtIkha8QuSsRT8pHei2JPVvfUB8=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Extensions.DependencyInjection.Abstractions.dll" size="63784">
|
||||
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" version="8.0.0.2" publicKeyToken="ADB9793829DDAE60" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>Ge3U9eaaRYncMUkJtYVKIvicS5qwQFkXcTl7LLGhe6Y=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Extensions.Logging.Abstractions.dll" size="67880">
|
||||
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" version="8.0.0.2" publicKeyToken="ADB9793829DDAE60" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>QBBtlRffKDtnxOXKG4YRVpA7fmvyDMzbFmSVRDFdQu4=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Extensions.Options.dll" size="68768">
|
||||
<assemblyIdentity name="Microsoft.Extensions.Options" version="8.0.0.2" publicKeyToken="ADB9793829DDAE60" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>c3DHL4ddmRGH4ntasuwkQro05v8nlQFTxmlq7JBVPF0=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Extensions.Primitives.dll" size="49936">
|
||||
<assemblyIdentity name="Microsoft.Extensions.Primitives" version="8.0.0.0" publicKeyToken="ADB9793829DDAE60" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>PG6lVZRDPi8DQCJ70ViPurG6ihDJkxm1KMxVcyWTQMs=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Office.Tools.Common.v4.0.Utilities.dll" size="32664">
|
||||
<assemblyIdentity name="Microsoft.Office.Tools.Common.v4.0.Utilities" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>jLCTF8Mm6bD4PDN+rnzN6q0+ReXaNgPh68kMWgatFwI=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Microsoft.Office.Tools.Outlook.v4.0.Utilities.dll" size="49048">
|
||||
<assemblyIdentity name="Microsoft.Office.Tools.Outlook.v4.0.Utilities" version="10.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>AY9f4ogMVBnNqNKvGc0Ko8U3XsIDeLhU/c5jky7x2Zc=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Polly.dll" size="294960">
|
||||
<assemblyIdentity name="Polly" version="8.0.0.0" publicKeyToken="C8A3FFC3F8F825CC" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>01vtaLqFx2ZiynhO4GgatEAd0Y1XmUo1AMI+CSIy5Ag=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Polly.Core.dll" size="241712">
|
||||
<assemblyIdentity name="Polly.Core" version="8.0.0.0" publicKeyToken="C8A3FFC3F8F825CC" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>rxJ4TrxMsT8aI+YQY5A92tKKH4VDZnPLN6QPyHLgZsI=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Serilog.dll" size="154112">
|
||||
<assemblyIdentity name="Serilog" version="4.0.0.0" publicKeyToken="24C2F752A8E58A10" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>E2b/L9D/pZ/Wsjo3IVAHEvDqtyucnyzPiJ0rn4CBiOQ=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="Serilog.Sinks.File.dll" size="32768">
|
||||
<assemblyIdentity name="Serilog.Sinks.File" version="6.0.0.0" publicKeyToken="24C2F752A8E58A10" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>yZtkok6s9F6xTZBKEpT9bm3EbA/+YcU+p7nFj1TNsOM=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Buffers.dll" size="20856">
|
||||
<assemblyIdentity name="System.Buffers" version="4.0.3.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>rMzPvkXZ8I/+7ZkW43sz6YxlvgEs//bn+ntnIQzh/vs=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.ComponentModel.Annotations.dll" size="43152">
|
||||
<assemblyIdentity name="System.ComponentModel.Annotations" version="4.2.1.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>BBE8hUkYVRnzICeQzrI99glkSHK5wkmlbSvPWVZhAsQ=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Diagnostics.DiagnosticSource.dll" size="189104">
|
||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" version="8.0.0.1" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>GIdZJhX8CLvJGtYjB8jBHfwUxFvxINJx1mvkoNGhSac=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Memory.dll" size="142240">
|
||||
<assemblyIdentity name="System.Memory" version="4.0.1.2" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>vz+4RmT0CX8aipvHGlHc+M8akF1AgKTSkNoXMIZuhW8=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Numerics.Vectors.dll" size="115856">
|
||||
<assemblyIdentity name="System.Numerics.Vectors" version="4.1.4.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>HT74aYKB589zcdFVSv71hys5+Wwm2nciEKM9oEG6EYM=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Runtime.CompilerServices.Unsafe.dll" size="18024">
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" version="6.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>N3aEiOjvRXKbx9miZ3YzxkUAQpdbuWUW4YbabLnNDc8=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Security.Cryptography.ProtectedData.dll" size="21776">
|
||||
<assemblyIdentity name="System.Security.Cryptography.ProtectedData" version="8.0.0.0" publicKeyToken="B03F5F7F11D50A3A" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>N/qb7rtPZhOmNTeKfhRfkYfgk0cboq7VzfmuFIdKX3s=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Text.Encodings.Web.dll" size="79024">
|
||||
<assemblyIdentity name="System.Text.Encodings.Web" version="8.0.0.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>6cT17tGGyxKcUnxLjWfRY+ovI5bp2LluMLXnwSIDzoQ=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Text.Json.dll" size="644888">
|
||||
<assemblyIdentity name="System.Text.Json" version="8.0.0.5" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>hsPyY66bRGmrEmbIBHEIcIJEfrSjjmuXv16E3hXAeh0=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Threading.Channels.dll" size="74000">
|
||||
<assemblyIdentity name="System.Threading.Channels" version="8.0.0.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>pC5FobuOZSjSRPhhHPHj5zHKtIGmeSjBvoFeY1/+jsc=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.Threading.Tasks.Extensions.dll" size="25984">
|
||||
<assemblyIdentity name="System.Threading.Tasks.Extensions" version="4.2.0.1" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>T4H/0NxyBNt1r8NepCkXabB8RAWS8oiUJg7qdmJqI8Y=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="System.ValueTuple.dll" size="25232">
|
||||
<assemblyIdentity name="System.ValueTuple" version="4.0.3.0" publicKeyToken="CC7B13FFCD2DDD51" language="neutral" processorArchitecture="msil" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>6QXRAlhbIsbfBPIZr1y9v6e8Fll56XiLYt9tzBZeEPQ=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<file name="MarcusLaw.OutlookAddin.dll.config" size="1827">
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>uMegIvfUIIUq0XcXNVowCSN4iTs7XAuPmHY2Gb/9AR4=</dsig:DigestValue>
|
||||
</hash>
|
||||
</file>
|
||||
<vstav3:addIn xmlns:vstav3="urn:schemas-microsoft-com:vsta.v3">
|
||||
<vstav3:entryPointsCollection>
|
||||
<vstav3:entryPoints>
|
||||
<vstav3:entryPoint class="OutlookAddin.ThisAddIn">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin" version="1.0.7.0" language="neutral" processorArchitecture="msil" />
|
||||
</vstav3:entryPoint>
|
||||
</vstav3:entryPoints>
|
||||
</vstav3:entryPointsCollection>
|
||||
<vstav3:update enabled="true">
|
||||
<vstav3:expiration maximumAge="7" unit="days" />
|
||||
</vstav3:update>
|
||||
<vstav3:application>
|
||||
<vstov4:customizations xmlns:vstov4="urn:schemas-microsoft-com:vsto.v4">
|
||||
<vstov4:customization>
|
||||
<vstov4:appAddIn application="Outlook" loadBehavior="3" keyName="MarcusLaw.OutlookAddin">
|
||||
<vstov4:friendlyName>MarcusLaw.OutlookAddin</vstov4:friendlyName>
|
||||
<vstov4:description>MarcusLaw.OutlookAddin</vstov4:description>
|
||||
<vstov4.1:ribbonTypes xmlns:vstov4.1="urn:schemas-microsoft-com:vsto.v4.1" />
|
||||
</vstov4:appAddIn>
|
||||
</vstov4:customization>
|
||||
</vstov4:customizations>
|
||||
</vstav3:application>
|
||||
</vstav3:addIn>
|
||||
<publisherIdentity name="CN=HP-OFFICE1\Chaim" issuerKeyHash="461191bb791c8dcf6417521bc4cc018623c19538" /><Signature Id="StrongNameSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /><DigestValue>wM13VCTBORPvZvtVa3YF66dro2pCcN8pucJyy+jTfoA=</DigestValue></Reference></SignedInfo><SignatureValue>dsDElqrcVTLfCKtMxmY/j7UatomYO80a72g8QUQgqIe4KmJyTr3gQkmu18UjhT2+mTbTIahwj03kIVNnAiFQK0FkGcK7HI758be3Wo0Ib3RDJevFegV/uNH5U4SImdPk1mHFoV7RuqB08ioQtHKd1VWi/vaEc8EVFCgRnLANrL4=</SignatureValue><KeyInfo Id="StrongNameKeyInfo"><KeyValue><RSAKeyValue><Modulus>xREBVt621l5WQ3HNT3kMrcZSyVcrSzXGDKTMvBwbaOmt7ZSsM5F8hZ+AQTmTC2BoXM8LeiP+wZAVU0MYokbWdHIG9TSqT5Uaud77qpDIfgJKk24CMS0ptE1PI4usLdwO1KyfRESHW8gxgVt0hAWuDudgI/EnOiyM5YRZNEf8Di0=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue><msrel:RelData xmlns:msrel="http://schemas.microsoft.com/windows/rel/2005/reldata"><r:license xmlns:r="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:as="http://schemas.microsoft.com/windows/pki/2005/Authenticode"><r:grant><as:ManifestInformation Hash="807ed3e8cb72c2b929df70426aa36ba7eb05766b55fb66ef1339c1245477cdc0" Description="" Url=""><as:assemblyIdentity name="MarcusLaw.OutlookAddin.dll" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" type="win32" /></as:ManifestInformation><as:SignedBy /><as:AuthenticodePublisher><as:X509SubjectName>CN=HP-OFFICE1\Chaim</as:X509SubjectName></as:AuthenticodePublisher></r:grant><r:issuer><Signature Id="AuthenticodeSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /><DigestValue>UgZbGtjRWoDrCAiV0IY5v0u4waIeDBt21bIbnTo2Cmk=</DigestValue></Reference></SignedInfo><SignatureValue>rksSK/JiE2/Zsf1ESCzwLUilq+7Vv9kThYx7oxnWU7vzliAUhTRl/smA0NNZqJLlrQ0KmNP79OR0Z4c9o7hnGr+q6fUfjoevcSKdSSaHkrLMU70b799w+A2mP3Bn01znZjncPW5/WUGq5euehFDkfCaPU9IEGKx62XFjh8QIq7c=</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>xREBVt621l5WQ3HNT3kMrcZSyVcrSzXGDKTMvBwbaOmt7ZSsM5F8hZ+AQTmTC2BoXM8LeiP+wZAVU0MYokbWdHIG9TSqT5Uaud77qpDIfgJKk24CMS0ptE1PI4usLdwO1KyfRESHW8gxgVt0hAWuDudgI/EnOiyM5YRZNEf8Di0=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>MIIB2TCCAUKgAwIBAgIQc4FF18qP9L1B37xzsUPl3jANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDHiAASABQAC0ATwBGAEYASQBDAEUAMQBcAEMAaABhAGkAbTAeFw0yNjA1MTExMTQ0MDlaFw0yNzA1MTExNzQ0MDlaMCsxKTAnBgNVBAMeIABIAFAALQBPAEYARgBJAEMARQAxAFwAQwBoAGEAaQBtMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFEQFW3rbWXlZDcc1PeQytxlLJVytLNcYMpMy8HBto6a3tlKwzkXyFn4BBOZMLYGhczwt6I/7BkBVTQxiiRtZ0cgb1NKpPlRq53vuqkMh+AkqTbgIxLSm0TU8ji6wt3A7UrJ9ERIdbyDGBW3SEBa4O52Aj8Sc6LIzlhFk0R/wOLQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFqX6W2LgF51biJUxrAe0KCZRG1YJsBhXyKf3RUDIWbyMuYmiwfIGKSEi5SX1wOD+4dF6+tBclK9pGOgqqNsfro67Mhdoo2HUhnrRXPUeqjgo5NR0qbxVYszhFBBBCjCxrDVjh8j7/lutwWUWGnF9mc3Bcw5GIHMZ+3d9T4IUYpI</X509Certificate></X509Data></KeyInfo></Signature></r:issuer></r:license></msrel:RelData></KeyInfo></Signature></asmv1:assembly>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xrml="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.vsto" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
|
||||
<description asmv2:publisher="MarcusLaw.OutlookAddin" asmv2:product="MarcusLaw.OutlookAddin" xmlns="urn:schemas-microsoft-com:asm.v1" />
|
||||
<deployment install="false" mapFileExtensions="true" />
|
||||
<compatibleFrameworks xmlns="urn:schemas-microsoft-com:clickonce.v2">
|
||||
<framework targetVersion="4.8" profile="Full" supportedRuntime="4.0.30319" />
|
||||
</compatibleFrameworks>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" codebase="Application Files\MarcusLaw.OutlookAddin_1_0_7_0\MarcusLaw.OutlookAddin.dll.manifest" size="36548">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.dll" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" type="win32" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>oFrXd0JDtHBok3bcmFLDAa5KFf5jBZVRZPJUYiSPakQ=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<publisherIdentity name="CN=HP-OFFICE1\Chaim" issuerKeyHash="461191bb791c8dcf6417521bc4cc018623c19538" /><Signature Id="StrongNameSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /><DigestValue>AoiIvEZEDw5NCrymwVAvIW1PN4f6L/G/2Jw2WXuAemM=</DigestValue></Reference></SignedInfo><SignatureValue>dllu0k2mVVdH4KTiz7PQJQO5ZO3uO7Yzi5EGrzcM+SlFppRxFcu35Xgm82GLJ5UbsgzugIiHcq69HYsbYgqQ5KRAV2W2jFSLMbqXyXFTpGR3ZP/mV8DF9Od1Ha+Ul61ZPfLHqSsK+TyStc+P5DkiXnLkhKLqkUyWpSpVFlHGkNE=</SignatureValue><KeyInfo Id="StrongNameKeyInfo"><KeyValue><RSAKeyValue><Modulus>xREBVt621l5WQ3HNT3kMrcZSyVcrSzXGDKTMvBwbaOmt7ZSsM5F8hZ+AQTmTC2BoXM8LeiP+wZAVU0MYokbWdHIG9TSqT5Uaud77qpDIfgJKk24CMS0ptE1PI4usLdwO1KyfRESHW8gxgVt0hAWuDudgI/EnOiyM5YRZNEf8Di0=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue><msrel:RelData xmlns:msrel="http://schemas.microsoft.com/windows/rel/2005/reldata"><r:license xmlns:r="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:as="http://schemas.microsoft.com/windows/pki/2005/Authenticode"><r:grant><as:ManifestInformation Hash="637a807b59369cd8bff12ffa87374f6d212f50c1a6bc0a4d0e0f4446bc888802" Description="" Url=""><as:assemblyIdentity name="MarcusLaw.OutlookAddin.vsto" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" /></as:ManifestInformation><as:SignedBy /><as:AuthenticodePublisher><as:X509SubjectName>CN=HP-OFFICE1\Chaim</as:X509SubjectName></as:AuthenticodePublisher></r:grant><r:issuer><Signature Id="AuthenticodeSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /><DigestValue>a2ZfU5jaiU9/spzQ38LrO3Oh5H1amjxMKA+UDGIxk88=</DigestValue></Reference></SignedInfo><SignatureValue>ASngBgbaDxF8ymTll++oets2qBL0KP7cDJTN34zncwwk2+I+ToCM0Mg7z1cwvQZftfxnKgcwKD5b57mIHIqj6Aklgyf3Bh5ifXN9kUIyNjHxhe905AYjBWHyKLQm+ld6V3eb+N8HJ7jYUewbevols9Mm3pQVYr8eWHL3FOp0dyA=</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>xREBVt621l5WQ3HNT3kMrcZSyVcrSzXGDKTMvBwbaOmt7ZSsM5F8hZ+AQTmTC2BoXM8LeiP+wZAVU0MYokbWdHIG9TSqT5Uaud77qpDIfgJKk24CMS0ptE1PI4usLdwO1KyfRESHW8gxgVt0hAWuDudgI/EnOiyM5YRZNEf8Di0=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>MIIB2TCCAUKgAwIBAgIQc4FF18qP9L1B37xzsUPl3jANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDHiAASABQAC0ATwBGAEYASQBDAEUAMQBcAEMAaABhAGkAbTAeFw0yNjA1MTExMTQ0MDlaFw0yNzA1MTExNzQ0MDlaMCsxKTAnBgNVBAMeIABIAFAALQBPAEYARgBJAEMARQAxAFwAQwBoAGEAaQBtMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFEQFW3rbWXlZDcc1PeQytxlLJVytLNcYMpMy8HBto6a3tlKwzkXyFn4BBOZMLYGhczwt6I/7BkBVTQxiiRtZ0cgb1NKpPlRq53vuqkMh+AkqTbgIxLSm0TU8ji6wt3A7UrJ9ERIdbyDGBW3SEBa4O52Aj8Sc6LIzlhFk0R/wOLQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFqX6W2LgF51biJUxrAe0KCZRG1YJsBhXyKf3RUDIWbyMuYmiwfIGKSEi5SX1wOD+4dF6+tBclK9pGOgqqNsfro67Mhdoo2HUhnrRXPUeqjgo5NR0qbxVYszhFBBBCjCxrDVjh8j7/lutwWUWGnF9mc3Bcw5GIHMZ+3d9T4IUYpI</X509Certificate></X509Data></KeyInfo></Signature></r:issuer></r:license></msrel:RelData></KeyInfo></Signature></asmv1:assembly>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xrml="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.vsto" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
|
||||
<description asmv2:publisher="MarcusLaw.OutlookAddin" asmv2:product="MarcusLaw.OutlookAddin" xmlns="urn:schemas-microsoft-com:asm.v1" />
|
||||
<deployment install="false" mapFileExtensions="true" />
|
||||
<compatibleFrameworks xmlns="urn:schemas-microsoft-com:clickonce.v2">
|
||||
<framework targetVersion="4.8" profile="Full" supportedRuntime="4.0.30319" />
|
||||
</compatibleFrameworks>
|
||||
<dependency>
|
||||
<dependentAssembly dependencyType="install" codebase="Application Files\MarcusLaw.OutlookAddin_1_0_7_0\MarcusLaw.OutlookAddin.dll.manifest" size="36548">
|
||||
<assemblyIdentity name="MarcusLaw.OutlookAddin.dll" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" type="win32" />
|
||||
<hash>
|
||||
<dsig:Transforms>
|
||||
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
|
||||
</dsig:Transforms>
|
||||
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
|
||||
<dsig:DigestValue>oFrXd0JDtHBok3bcmFLDAa5KFf5jBZVRZPJUYiSPakQ=</dsig:DigestValue>
|
||||
</hash>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<publisherIdentity name="CN=HP-OFFICE1\Chaim" issuerKeyHash="461191bb791c8dcf6417521bc4cc018623c19538" /><Signature Id="StrongNameSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /><DigestValue>AoiIvEZEDw5NCrymwVAvIW1PN4f6L/G/2Jw2WXuAemM=</DigestValue></Reference></SignedInfo><SignatureValue>dllu0k2mVVdH4KTiz7PQJQO5ZO3uO7Yzi5EGrzcM+SlFppRxFcu35Xgm82GLJ5UbsgzugIiHcq69HYsbYgqQ5KRAV2W2jFSLMbqXyXFTpGR3ZP/mV8DF9Od1Ha+Ul61ZPfLHqSsK+TyStc+P5DkiXnLkhKLqkUyWpSpVFlHGkNE=</SignatureValue><KeyInfo Id="StrongNameKeyInfo"><KeyValue><RSAKeyValue><Modulus>xREBVt621l5WQ3HNT3kMrcZSyVcrSzXGDKTMvBwbaOmt7ZSsM5F8hZ+AQTmTC2BoXM8LeiP+wZAVU0MYokbWdHIG9TSqT5Uaud77qpDIfgJKk24CMS0ptE1PI4usLdwO1KyfRESHW8gxgVt0hAWuDudgI/EnOiyM5YRZNEf8Di0=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue><msrel:RelData xmlns:msrel="http://schemas.microsoft.com/windows/rel/2005/reldata"><r:license xmlns:r="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:as="http://schemas.microsoft.com/windows/pki/2005/Authenticode"><r:grant><as:ManifestInformation Hash="637a807b59369cd8bff12ffa87374f6d212f50c1a6bc0a4d0e0f4446bc888802" Description="" Url=""><as:assemblyIdentity name="MarcusLaw.OutlookAddin.vsto" version="1.0.7.0" publicKeyToken="41d8d795775ea8cb" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" /></as:ManifestInformation><as:SignedBy /><as:AuthenticodePublisher><as:X509SubjectName>CN=HP-OFFICE1\Chaim</as:X509SubjectName></as:AuthenticodePublisher></r:grant><r:issuer><Signature Id="AuthenticodeSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" /><DigestValue>a2ZfU5jaiU9/spzQ38LrO3Oh5H1amjxMKA+UDGIxk88=</DigestValue></Reference></SignedInfo><SignatureValue>ASngBgbaDxF8ymTll++oets2qBL0KP7cDJTN34zncwwk2+I+ToCM0Mg7z1cwvQZftfxnKgcwKD5b57mIHIqj6Aklgyf3Bh5ifXN9kUIyNjHxhe905AYjBWHyKLQm+ld6V3eb+N8HJ7jYUewbevols9Mm3pQVYr8eWHL3FOp0dyA=</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>xREBVt621l5WQ3HNT3kMrcZSyVcrSzXGDKTMvBwbaOmt7ZSsM5F8hZ+AQTmTC2BoXM8LeiP+wZAVU0MYokbWdHIG9TSqT5Uaud77qpDIfgJKk24CMS0ptE1PI4usLdwO1KyfRESHW8gxgVt0hAWuDudgI/EnOiyM5YRZNEf8Di0=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>MIIB2TCCAUKgAwIBAgIQc4FF18qP9L1B37xzsUPl3jANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDHiAASABQAC0ATwBGAEYASQBDAEUAMQBcAEMAaABhAGkAbTAeFw0yNjA1MTExMTQ0MDlaFw0yNzA1MTExNzQ0MDlaMCsxKTAnBgNVBAMeIABIAFAALQBPAEYARgBJAEMARQAxAFwAQwBoAGEAaQBtMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFEQFW3rbWXlZDcc1PeQytxlLJVytLNcYMpMy8HBto6a3tlKwzkXyFn4BBOZMLYGhczwt6I/7BkBVTQxiiRtZ0cgb1NKpPlRq53vuqkMh+AkqTbgIxLSm0TU8ji6wt3A7UrJ9ERIdbyDGBW3SEBa4O52Aj8Sc6LIzlhFk0R/wOLQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFqX6W2LgF51biJUxrAe0KCZRG1YJsBhXyKf3RUDIWbyMuYmiwfIGKSEi5SX1wOD+4dF6+tBclK9pGOgqqNsfro67Mhdoo2HUhnrRXPUeqjgo5NR0qbxVYszhFBBBCjCxrDVjh8j7/lutwWUWGnF9mc3Bcw5GIHMZ+3d9T4IUYpI</X509Certificate></X509Data></KeyInfo></Signature></r:issuer></r:license></msrel:RelData></KeyInfo></Signature></asmv1:assembly>
|
||||
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Response from POST /api/v1/Attachment — the server returns at least
|
||||
/// the new attachment's id, which we then thread into a Document create.
|
||||
/// </summary>
|
||||
public sealed class AttachmentEntity
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,13 @@ namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
// Marcus-Law custom field on Case — court file/docket number.
|
||||
// Field name guessed from EspoCRM "c-prefixed PascalCase" convention;
|
||||
// adjust if the admin used a different name.
|
||||
[JsonPropertyName("cCourtCaseNumber")]
|
||||
[JsonConverter(typeof(FlexibleStringConverter))]
|
||||
public string? CourtCaseNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("accountId")]
|
||||
public string? AccountId { get; set; }
|
||||
|
||||
@@ -31,5 +38,12 @@ namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
// Set by Marcus-Law's NetworkStorageIntegration hook on first
|
||||
// Document save. Empty until a document has been written through
|
||||
// EspoCRM. When empty we compute it client-side from number +
|
||||
// primary contact name.
|
||||
[JsonPropertyName("networkStorageFolderPath")]
|
||||
public string? NetworkStorageFolderPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
{
|
||||
public sealed class DocumentEntity
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("fileId")]
|
||||
public string? FileId { get; set; }
|
||||
|
||||
[JsonPropertyName("folderId")]
|
||||
public string? FolderId { get; set; }
|
||||
|
||||
[JsonPropertyName("folderName")]
|
||||
public string? FolderName { get; set; }
|
||||
|
||||
[JsonPropertyName("parentType")]
|
||||
public string? ParentType { get; set; }
|
||||
|
||||
[JsonPropertyName("parentId")]
|
||||
public string? ParentId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A row from /api/v1/DocumentFolder — used to populate the folder
|
||||
/// dropdown in the "Save attachments" dialog. EspoCRM document folders
|
||||
/// are organizational labels independent of the document's parent link.
|
||||
/// </summary>
|
||||
public sealed class DocumentFolder
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("parentId")]
|
||||
public string? ParentId { get; set; }
|
||||
|
||||
[JsonPropertyName("order")]
|
||||
public int? Order { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,13 @@ namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
|
||||
public int CandidateCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="IsAmbiguous"/> is true, holds the candidate
|
||||
/// contacts that share the sender's email so the sidebar can show
|
||||
/// a picker instead of bouncing the user into manual search.
|
||||
/// </summary>
|
||||
public List<ContactEntity> Candidates { get; set; } = new List<ContactEntity>();
|
||||
|
||||
public DateTimeOffset RetrievedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// One row from GET /NetworkStorage/folderContents — folder or file.
|
||||
/// </summary>
|
||||
public sealed class NetworkStorageItem
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty; // "folder" or "file"
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonPropertyName("modified")]
|
||||
public string? Modified { get; set; }
|
||||
|
||||
public bool IsFolder => string.Equals(Type, "folder", System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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 AttachmentSaveService : IAttachmentSaveService
|
||||
{
|
||||
private readonly IEspoCrmClient _client;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public AttachmentSaveService(IEspoCrmClient client, ILogger logger)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<AttachmentSaveSummary> SaveAsync(
|
||||
IReadOnlyList<EspoAttachment> attachments,
|
||||
string targetFolderPath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (attachments == null) throw new ArgumentNullException(nameof(attachments));
|
||||
if (string.IsNullOrWhiteSpace(targetFolderPath)) throw new ArgumentException("targetFolderPath required", nameof(targetFolderPath));
|
||||
|
||||
var summary = new AttachmentSaveSummary();
|
||||
if (attachments.Count == 0) return summary;
|
||||
|
||||
var trimmedFolder = targetFolderPath.TrimEnd('/');
|
||||
|
||||
foreach (var att in attachments)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (summary.AuthRequired)
|
||||
{
|
||||
summary.Items.Add(new AttachmentSaveItemResult
|
||||
{
|
||||
FileName = att.Name,
|
||||
Outcome = AttachmentSaveOutcome.AuthRequired,
|
||||
ErrorMessage = "Skipped — earlier upload rejected by EspoCRM"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: create the EspoCRM Attachment (binary stored
|
||||
// in data/upload/<id>; not yet on the network share).
|
||||
var uploaded = await _client.UploadAttachmentAsync(
|
||||
att.Name, att.ContentType, att.ContentBase64, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Step 2: ask Marcus-Law's NetworkStorageIntegration
|
||||
// to place the uploaded blob under <targetFolderPath>.
|
||||
// The integration appends the filename to the path.
|
||||
await _client.UploadAttachmentToNetworkStorageAsync(
|
||||
uploaded.Id, trimmedFolder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Step 3: verify the file actually landed on the share —
|
||||
// the server can return 200 yet leave nothing on disk
|
||||
// when a downstream WebDAV proxy silently fails. Read
|
||||
// the folder back and look for our filename.
|
||||
await VerifyArrivedAsync(att.Name, trimmedFolder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.Information(
|
||||
"Saved attachment {Name} → {Path} (attachmentId={AttachmentId})",
|
||||
att.Name, trimmedFolder, uploaded.Id);
|
||||
|
||||
summary.Items.Add(new AttachmentSaveItemResult
|
||||
{
|
||||
FileName = att.Name,
|
||||
Outcome = AttachmentSaveOutcome.Saved,
|
||||
AttachmentId = uploaded.Id
|
||||
});
|
||||
}
|
||||
catch (EspoCrmAuthorizationException ex)
|
||||
{
|
||||
_logger.Warning(ex, "Attachment save aborted: EspoCRM rejected credentials");
|
||||
summary.AuthRequired = true;
|
||||
summary.Items.Add(new AttachmentSaveItemResult
|
||||
{
|
||||
FileName = att.Name,
|
||||
Outcome = AttachmentSaveOutcome.AuthRequired,
|
||||
ErrorMessage = ex.Message
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Attachment save failed for {Name}", att.Name);
|
||||
summary.Items.Add(new AttachmentSaveItemResult
|
||||
{
|
||||
FileName = att.Name,
|
||||
Outcome = AttachmentSaveOutcome.Failed,
|
||||
ErrorMessage = ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post-upload sanity check: list the folder we just wrote into
|
||||
/// and confirm a file with the same (possibly sanitized) name
|
||||
/// shows up. Diagnostic only — does not change the outcome. If
|
||||
/// the file isn't there, log a WARNING with everything that IS
|
||||
/// in the folder so we can spot the actual server-side path.
|
||||
/// </summary>
|
||||
private async Task VerifyArrivedAsync(string expectedName, string folderPath, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = await _client.ListNetworkStorageFolderAsync(folderPath, ct).ConfigureAwait(false);
|
||||
var trimmedExpected = expectedName.Trim().TrimStart('-', ' ').TrimEnd('-', ' ');
|
||||
bool found = false;
|
||||
foreach (var it in items)
|
||||
{
|
||||
if (it.IsFolder) continue;
|
||||
if (string.Equals(it.Name, expectedName, StringComparison.Ordinal) ||
|
||||
string.Equals(it.Name, trimmedExpected, StringComparison.Ordinal))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
var names = new List<string>();
|
||||
foreach (var it in items)
|
||||
{
|
||||
if (it.IsFolder) continue;
|
||||
names.Add(it.Name);
|
||||
}
|
||||
_logger.Warning(
|
||||
"Post-upload verify: expected '{Expected}' under '{Path}' but it's NOT in the listing. " +
|
||||
"Server reported the upload as successful but the file is not on the share. " +
|
||||
"Files currently in that folder: [{Files}]",
|
||||
expectedName, folderPath, string.Join(", ", names));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Information("Post-upload verify: '{Expected}' present under '{Path}'", expectedName, folderPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Post-upload verify failed for '{Expected}' under '{Path}'", expectedName, folderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,320 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
public Task<UserInfo> TestConnectionAsync(CancellationToken cancellationToken = default)
|
||||
=> GetJsonAsync<UserInfo>("App/user", cancellationToken);
|
||||
|
||||
public async Task<AttachmentEntity> UploadAttachmentAsync(string fileName, string contentType, string base64Content, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName required", nameof(fileName));
|
||||
if (string.IsNullOrWhiteSpace(base64Content)) throw new ArgumentException("base64Content required", nameof(base64Content));
|
||||
|
||||
var mime = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType;
|
||||
var dataUri = "data:" + mime + ";base64," + base64Content;
|
||||
// Per the official EspoCRM API docs (POST /Attachment), the binary
|
||||
// is sent in the "file" field as a data-URI. The previous extra
|
||||
// "contents" alias was an undocumented legacy that strict tenants
|
||||
// reject with 400.
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = fileName,
|
||||
["type"] = mime,
|
||||
["role"] = "Attachment",
|
||||
["relatedType"] = "Document",
|
||||
["field"] = "file",
|
||||
["file"] = dataUri
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
_logger.Information(
|
||||
"UploadAttachment POST /Attachment (name={Name}, type={Type}, base64Length={Length})",
|
||||
fileName, mime, base64Content.Length);
|
||||
|
||||
var response = await ExecuteAsync(HttpMethod.Post, "Attachment", json, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errBody = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var reason = string.Join(",", response.Headers.TryGetValues("X-Status-Reason", out var v) ? v : Array.Empty<string>());
|
||||
_logger.Warning(
|
||||
"UploadAttachment failed: {Status} (X-Status-Reason={Reason}) body={Body}",
|
||||
response.StatusCode, reason, string.IsNullOrEmpty(errBody) ? "(empty)" : Truncate(errBody, 500));
|
||||
// 401 only = auth. 403 here is content/permission, not creds.
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new EspoCrmAuthorizationException(
|
||||
$"EspoCRM rejected credentials ({(int)response.StatusCode} {response.StatusCode}). Re-enter API key.");
|
||||
}
|
||||
throw new EspoCrmHttpException(response.StatusCode, errBody,
|
||||
$"EspoCRM Attachment upload returned {(int)response.StatusCode} {response.StatusCode}" +
|
||||
(string.IsNullOrEmpty(reason) ? "" : " (reason: " + reason + ")"));
|
||||
}
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize<AttachmentEntity>(body, JsonOptions)
|
||||
?? throw new EspoCrmHttpException(response.StatusCode, body, "Empty AttachmentEntity from EspoCRM");
|
||||
}
|
||||
finally
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DocumentEntity> CreateDocumentAsync(string name, string attachmentFileId, string? parentType, string? parentId, string? folderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name required", nameof(name));
|
||||
if (string.IsNullOrWhiteSpace(attachmentFileId)) throw new ArgumentException("attachmentFileId required", nameof(attachmentFileId));
|
||||
|
||||
// Marcus-Law's EspoCRM has `publishDate` configured as required on
|
||||
// Document. The field accepts a plain date (YYYY-MM-DD). Default
|
||||
// to today; the user can edit it later in EspoCRM if needed.
|
||||
var todayIso = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = name,
|
||||
["fileId"] = attachmentFileId,
|
||||
["status"] = "Active",
|
||||
["publishDate"] = todayIso
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(parentType)) payload["parentType"] = parentType;
|
||||
if (!string.IsNullOrWhiteSpace(parentId)) payload["parentId"] = parentId;
|
||||
if (!string.IsNullOrWhiteSpace(folderId)) payload["folderId"] = folderId;
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
_logger.Information(
|
||||
"CreateDocument POST /Document (name={Name}, fileId={FileId}, parent={ParentType}/{ParentId}, folder={FolderId})",
|
||||
name, attachmentFileId, parentType, parentId, folderId ?? "(none)");
|
||||
|
||||
var response = await ExecuteAsync(HttpMethod.Post, "Document", json, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errBody = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var reason = response.Headers.TryGetValues("X-Status-Reason", out var v)
|
||||
? string.Join(",", v) : "(none)";
|
||||
_logger.Warning(
|
||||
"CreateDocument failed: {Status} (X-Status-Reason={Reason}) body={Body}",
|
||||
response.StatusCode, reason, string.IsNullOrEmpty(errBody) ? "(empty)" : Truncate(errBody, 500));
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new EspoCrmAuthorizationException(
|
||||
$"EspoCRM rejected credentials ({(int)response.StatusCode} {response.StatusCode}). Re-enter API key.");
|
||||
}
|
||||
throw new EspoCrmHttpException(response.StatusCode, errBody,
|
||||
$"EspoCRM Document create returned {(int)response.StatusCode} {response.StatusCode}" +
|
||||
(string.IsNullOrEmpty(reason) ? "" : " (reason: " + reason + ")"));
|
||||
}
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize<DocumentEntity>(body, JsonOptions)
|
||||
?? throw new EspoCrmHttpException(response.StatusCode, body, "Empty DocumentEntity from EspoCRM");
|
||||
}
|
||||
finally
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<DocumentFolder>> ListFoldersUsedInCaseAsync(string caseId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(caseId)) return new List<DocumentFolder>();
|
||||
|
||||
// Some EspoCRM installs reject filtering by polymorphic parentType
|
||||
// with a 400. ParentId is a regular string field, and an ID alone
|
||||
// is unique enough — we'll filter just by that. We additionally
|
||||
// verify parentType client-side to be safe.
|
||||
var path =
|
||||
"Document?select=" + Uri.EscapeDataString("id,folderId,folderName,parentType") +
|
||||
"&where[0][type]=equals" +
|
||||
"&where[0][attribute]=parentId" +
|
||||
"&where[0][value]=" + Uri.EscapeDataString(caseId) +
|
||||
"&maxSize=200";
|
||||
|
||||
// Hand-rolled error capture (instead of GetJsonAsync) so a 400 from
|
||||
// a picky tenant ends up in the log with body + X-Status-Reason.
|
||||
HttpResponseMessage? response = null;
|
||||
try
|
||||
{
|
||||
response = await ExecuteAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errBody = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var reason = response.Headers.TryGetValues("X-Status-Reason", out var v)
|
||||
? string.Join(",", v) : "(none)";
|
||||
_logger.Warning(
|
||||
"ListFoldersUsedInCase({CaseId}): {Status} (X-Status-Reason={Reason}) body={Body}",
|
||||
caseId, response.StatusCode, reason,
|
||||
string.IsNullOrEmpty(errBody) ? "(empty)" : Truncate(errBody, 500));
|
||||
return new List<DocumentFolder>();
|
||||
}
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var parsed = JsonSerializer.Deserialize<EspoListResponse<DocumentEntity>>(body, JsonOptions)
|
||||
?? new EspoListResponse<DocumentEntity>();
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
var folders = new List<DocumentFolder>();
|
||||
foreach (var doc in parsed.List)
|
||||
{
|
||||
// Defensive: if the server returns docs from other parents
|
||||
// (shouldn't, but ID collisions are theoretically possible),
|
||||
// drop them.
|
||||
if (!string.IsNullOrEmpty(doc.ParentType) &&
|
||||
!string.Equals(doc.ParentType, "Case", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrEmpty(doc.FolderId)) continue;
|
||||
if (!seen.Add(doc.FolderId!)) continue;
|
||||
folders.Add(new DocumentFolder
|
||||
{
|
||||
Id = doc.FolderId!,
|
||||
Name = string.IsNullOrWhiteSpace(doc.FolderName) ? doc.FolderId! : doc.FolderName!
|
||||
});
|
||||
}
|
||||
_logger.Information(
|
||||
"ListFoldersUsedInCase({CaseId}) → {DocumentCount} documents, {FolderCount} distinct folders",
|
||||
caseId, parsed.List.Count, folders.Count);
|
||||
return folders;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "ListFoldersUsedInCaseAsync({CaseId}) failed", caseId);
|
||||
return new List<DocumentFolder>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
response?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureCaseSubfoldersAsync(string caseId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(caseId)) throw new ArgumentException("caseId required", nameof(caseId));
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["entityType"] = "Case",
|
||||
["entityId"] = caseId
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
var response = await ExecuteAsync(HttpMethod.Post, "NetworkStorage/action/createSubfolders", json, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var reason = response.Headers.TryGetValues("X-Status-Reason", out var v) ? string.Join(",", v) : "(none)";
|
||||
_logger.Warning(
|
||||
"EnsureCaseSubfolders({CaseId}): {Status} (X-Status-Reason={Reason}) body={Body}",
|
||||
caseId, response.StatusCode, reason, string.IsNullOrEmpty(body) ? "(empty)" : Truncate(body, 500));
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new EspoCrmAuthorizationException("EspoCRM rejected credentials. Re-enter API key.");
|
||||
}
|
||||
// Don't throw — folder creation is best-effort. Upload will fail
|
||||
// with its own clear error if the parent really isn't there.
|
||||
return;
|
||||
}
|
||||
_logger.Information("EnsureCaseSubfolders({CaseId}) OK", caseId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<NetworkStorageItem>> ListNetworkStorageFolderAsync(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = "NetworkStorage/folderContents";
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
query += "?path=" + Uri.EscapeDataString(path);
|
||||
}
|
||||
HttpResponseMessage? response = null;
|
||||
try
|
||||
{
|
||||
response = await ExecuteAsync(HttpMethod.Get, query, null, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var reason = response.Headers.TryGetValues("X-Status-Reason", out var v) ? string.Join(",", v) : "(none)";
|
||||
_logger.Warning(
|
||||
"ListNetworkStorageFolder({Path}): {Status} (X-Status-Reason={Reason}) body={Body}",
|
||||
path, response.StatusCode, reason, string.IsNullOrEmpty(body) ? "(empty)" : Truncate(body, 500));
|
||||
return new List<NetworkStorageItem>();
|
||||
}
|
||||
var json = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var items = JsonSerializer.Deserialize<List<NetworkStorageItem>>(json, JsonOptions);
|
||||
_logger.Information("ListNetworkStorageFolder({Path}) → {Count} entries", path, items?.Count ?? 0);
|
||||
return items ?? new List<NetworkStorageItem>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "ListNetworkStorageFolderAsync({Path}) failed", path);
|
||||
return new List<NetworkStorageItem>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
response?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadAttachmentToNetworkStorageAsync(string attachmentId, string targetPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attachmentId)) throw new ArgumentException("attachmentId required", nameof(attachmentId));
|
||||
if (string.IsNullOrWhiteSpace(targetPath)) throw new ArgumentException("targetPath required", nameof(targetPath));
|
||||
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["attachmentId"] = attachmentId,
|
||||
["path"] = targetPath
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
_logger.Information(
|
||||
"UploadToNetworkStorage POST /NetworkStorage/action/upload (attachmentId={AttachmentId}, path={Path})",
|
||||
attachmentId, targetPath);
|
||||
|
||||
var response = await ExecuteAsync(HttpMethod.Post, "NetworkStorage/action/upload", json, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
var reason = response.Headers.TryGetValues("X-Status-Reason", out var v) ? string.Join(",", v) : "(none)";
|
||||
_logger.Warning(
|
||||
"UploadToNetworkStorage failed: {Status} (X-Status-Reason={Reason}) body={Body}",
|
||||
response.StatusCode, reason, string.IsNullOrEmpty(body) ? "(empty)" : Truncate(body, 500));
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new EspoCrmAuthorizationException("EspoCRM rejected credentials. Re-enter API key.");
|
||||
}
|
||||
throw new EspoCrmHttpException(response.StatusCode, body,
|
||||
$"NetworkStorage upload returned {(int)response.StatusCode} {response.StatusCode}" +
|
||||
(string.IsNullOrEmpty(reason) || reason == "(none)" ? "" : " (reason: " + reason + ")"));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EspoListResponse<DocumentFolder>> ListDocumentFoldersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await GetJsonAsync<EspoListResponse<DocumentFolder>>(
|
||||
"DocumentFolder?orderBy=name&order=asc&maxSize=200",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
_logger.Information(
|
||||
"ListDocumentFoldersAsync → {Count} folders in tenant",
|
||||
result?.List?.Count ?? 0);
|
||||
return result ?? new EspoListResponse<DocumentFolder>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "ListDocumentFoldersAsync failed; returning empty list");
|
||||
return new EspoListResponse<DocumentFolder>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EspoListResponse<CaseEntity>> ListCasesForContactAsync(string contactId, int maxSize = 5, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contactId)) throw new ArgumentException("contactId required", nameof(contactId));
|
||||
@@ -343,14 +657,20 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
if (response.IsSuccessStatusCode) return;
|
||||
|
||||
var body = await ReadBodyAsync(response).ConfigureAwait(false);
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
|
||||
// Only 401 is unambiguously an authentication problem. 403 in
|
||||
// EspoCRM is "you are who you say you are, but THIS operation
|
||||
// isn't allowed" — content type rejected, ACL miss, required
|
||||
// role missing, etc. Conflating them aborts whole batches.
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new EspoCrmAuthorizationException(
|
||||
$"EspoCRM rejected credentials ({(int)response.StatusCode} {response.StatusCode}). Re-enter API key.");
|
||||
}
|
||||
|
||||
var reason = response.Headers.TryGetValues("X-Status-Reason", out var v)
|
||||
? " (reason: " + string.Join(",", v) + ")" : string.Empty;
|
||||
throw new EspoCrmHttpException(response.StatusCode, body,
|
||||
$"EspoCRM returned {(int)response.StatusCode} {response.StatusCode}: {Truncate(body, 500)}");
|
||||
$"EspoCRM returned {(int)response.StatusCode} {response.StatusCode}{reason}: {Truncate(body, 500)}");
|
||||
}
|
||||
|
||||
private static async Task<string> ReadBodyAsync(HttpResponseMessage response)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MarcusLaw.OutlookAddin.Core.Models;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
{
|
||||
public interface IAttachmentSaveService
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads each attachment as an EspoCRM Attachment, then asks
|
||||
/// Marcus-Law's NetworkStorage integration to place the file under
|
||||
/// <paramref name="targetFolderPath"/> on the shared storage. No
|
||||
/// Document entity is created — these are raw files dropped into
|
||||
/// the case's filesystem folder.
|
||||
/// </summary>
|
||||
/// <param name="attachments">files to save</param>
|
||||
/// <param name="targetFolderPath">
|
||||
/// Full relative folder path on storage, e.g.
|
||||
/// <c>"30-מרדכי שמביקו/אסמכתאות"</c>. The case folder + optional
|
||||
/// subfolder. Filename is appended internally.
|
||||
/// </param>
|
||||
Task<AttachmentSaveSummary> SaveAsync(
|
||||
IReadOnlyList<EspoAttachment> attachments,
|
||||
string targetFolderPath,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class AttachmentSaveSummary
|
||||
{
|
||||
public List<AttachmentSaveItemResult> Items { get; set; } = new List<AttachmentSaveItemResult>();
|
||||
|
||||
public bool AuthRequired { get; set; }
|
||||
|
||||
public int SavedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var n = 0;
|
||||
foreach (var it in Items)
|
||||
{
|
||||
if (it.Outcome == AttachmentSaveOutcome.Saved) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
public int FailedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var n = 0;
|
||||
foreach (var it in Items)
|
||||
{
|
||||
if (it.Outcome == AttachmentSaveOutcome.Failed) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AttachmentSaveItemResult
|
||||
{
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
public AttachmentSaveOutcome Outcome { get; set; }
|
||||
|
||||
public string? AttachmentId { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public enum AttachmentSaveOutcome
|
||||
{
|
||||
Saved,
|
||||
Failed,
|
||||
AuthRequired
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,16 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
Task<ContactEntity> CreateContactAsync(string name, string? emailAddress = null, CancellationToken cancellationToken = default);
|
||||
Task<EspoListResponse<CaseEntity>> ListCasesForContactAsync(string contactId, int maxSize = 5, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> TestConnectionAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AttachmentEntity> UploadAttachmentAsync(string fileName, string contentType, string base64Content, CancellationToken cancellationToken = default);
|
||||
Task<DocumentEntity> CreateDocumentAsync(string name, string attachmentFileId, string? parentType, string? parentId, string? folderId, CancellationToken cancellationToken = default);
|
||||
Task<EspoListResponse<DocumentFolder>> ListDocumentFoldersAsync(CancellationToken cancellationToken = default);
|
||||
Task<System.Collections.Generic.List<DocumentFolder>> ListFoldersUsedInCaseAsync(string caseId, CancellationToken cancellationToken = default);
|
||||
|
||||
// Marcus-Law NetworkStorageIntegration endpoints (filesystem-based
|
||||
// folder structure under the case folder).
|
||||
Task EnsureCaseSubfoldersAsync(string caseId, CancellationToken cancellationToken = default);
|
||||
Task<System.Collections.Generic.List<NetworkStorageItem>> ListNetworkStorageFolderAsync(string path, CancellationToken cancellationToken = default);
|
||||
Task UploadAttachmentToNetworkStorageAsync(string attachmentId, string targetPath, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
/// </summary>
|
||||
Task<MatchResult?> LookupAsync(string senderEmail, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the full contact + account + recent-cases bundle for a
|
||||
/// specific contact id. Used when the sender's email matches
|
||||
/// multiple contacts and the user picks one from the candidate
|
||||
/// list — we then drill into that contact's cases without
|
||||
/// dropping back to manual search.
|
||||
/// </summary>
|
||||
Task<MatchResult?> LookupContactAsync(string contactId, string? senderEmail = null, CancellationToken cancellationToken = default);
|
||||
|
||||
void Invalidate(string senderEmail);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MarcusLaw.OutlookAddin.Core.Models;
|
||||
@@ -44,60 +45,51 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
if (matches.Count > 1)
|
||||
// EspoCRM's /EmailAddress/search can return the same Contact
|
||||
// more than once when the contact has the searched-for
|
||||
// address registered under multiple emailAddressData rows.
|
||||
// Dedupe by Id so the sidebar shows one row per unique person.
|
||||
var distinctIds = matches
|
||||
.Where(m => !string.IsNullOrWhiteSpace(m.Id))
|
||||
.Select(m => m.Id)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Take(20)
|
||||
.ToList();
|
||||
|
||||
if (distinctIds.Count == 0)
|
||||
{
|
||||
var ambiguous = new MatchResult
|
||||
{
|
||||
SenderEmail = senderEmail,
|
||||
IsAmbiguous = true,
|
||||
CandidateCount = matches.Count
|
||||
};
|
||||
_cache.Set<MatchResult?>(key, ambiguous, CacheExpiry);
|
||||
return ambiguous;
|
||||
_cache.Set(key, (MatchResult?)null, CacheExpiry);
|
||||
return null;
|
||||
}
|
||||
|
||||
var contactRef = matches[0];
|
||||
var contact = await _client.GetContactAsync(contactRef.Id, ContactSelect, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AccountEntity? account = null;
|
||||
if (!string.IsNullOrWhiteSpace(contact.AccountId))
|
||||
if (distinctIds.Count == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
account = await _client.GetAccountAsync(contact.AccountId!, "id,name,emailAddress", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "MatchingService: failed to load account {AccountId} for {Email}", contact.AccountId, senderEmail);
|
||||
}
|
||||
var single = await LoadContactBundleAsync(distinctIds[0], senderEmail, cancellationToken).ConfigureAwait(false);
|
||||
_cache.Set<MatchResult?>(key, single, CacheExpiry);
|
||||
return single;
|
||||
}
|
||||
|
||||
var cases = new System.Collections.Generic.List<CaseEntity>();
|
||||
try
|
||||
{
|
||||
// Pull up to 50 cases per contact so the sidebar can show the
|
||||
// whole list, not just the 5 most recent. 50 is well below the
|
||||
// EspoCRM default page limit and large enough for any realistic
|
||||
// attorney/contact relationship.
|
||||
var list = await _client.ListCasesForContactAsync(contact.Id, 50, cancellationToken).ConfigureAwait(false);
|
||||
cases.AddRange(list.List);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "MatchingService: failed to list cases for {ContactId}", contact.Id);
|
||||
}
|
||||
// Two or more genuinely distinct contacts. Fetch each one's
|
||||
// full record (in parallel) so the picker can show real
|
||||
// name + account, not just the bare email payload.
|
||||
var fetched = await Task.WhenAll(
|
||||
distinctIds.Select(id => SafeGetContactAsync(id, cancellationToken))
|
||||
).ConfigureAwait(false);
|
||||
|
||||
var result = new MatchResult
|
||||
var candidates = fetched
|
||||
.Where(c => c != null && !string.IsNullOrWhiteSpace(c!.Id))
|
||||
.Select(c => c!)
|
||||
.ToList();
|
||||
|
||||
var ambiguous = new MatchResult
|
||||
{
|
||||
SenderEmail = senderEmail,
|
||||
Contact = contact,
|
||||
Account = account,
|
||||
RecentCases = cases,
|
||||
CandidateCount = 1
|
||||
IsAmbiguous = true,
|
||||
CandidateCount = candidates.Count,
|
||||
Candidates = candidates
|
||||
};
|
||||
|
||||
_cache.Set<MatchResult?>(key, result, CacheExpiry);
|
||||
return result;
|
||||
_cache.Set<MatchResult?>(key, ambiguous, CacheExpiry);
|
||||
return ambiguous;
|
||||
}
|
||||
catch (EspoCrmAuthorizationException)
|
||||
{
|
||||
@@ -114,6 +106,86 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MatchResult?> LookupContactAsync(string contactId, string? senderEmail = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contactId)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return await LoadContactBundleAsync(contactId, senderEmail ?? string.Empty, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (EspoCrmAuthorizationException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "MatchingService: LookupContactAsync failed for {ContactId}", contactId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MatchResult> LoadContactBundleAsync(string contactId, string senderEmail, CancellationToken cancellationToken)
|
||||
{
|
||||
var contact = await _client.GetContactAsync(contactId, ContactSelect, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AccountEntity? account = null;
|
||||
if (!string.IsNullOrWhiteSpace(contact.AccountId))
|
||||
{
|
||||
try
|
||||
{
|
||||
account = await _client.GetAccountAsync(contact.AccountId!, "id,name,emailAddress", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "MatchingService: failed to load account {AccountId} for contact {ContactId}", contact.AccountId, contactId);
|
||||
}
|
||||
}
|
||||
|
||||
var cases = new System.Collections.Generic.List<CaseEntity>();
|
||||
try
|
||||
{
|
||||
// Pull up to 50 cases per contact so the sidebar can show the
|
||||
// whole list, not just the 5 most recent. 50 is well below the
|
||||
// EspoCRM default page limit and large enough for any realistic
|
||||
// attorney/contact relationship.
|
||||
var list = await _client.ListCasesForContactAsync(contact.Id, 50, cancellationToken).ConfigureAwait(false);
|
||||
cases.AddRange(list.List);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "MatchingService: failed to list cases for {ContactId}", contact.Id);
|
||||
}
|
||||
|
||||
return new MatchResult
|
||||
{
|
||||
SenderEmail = senderEmail,
|
||||
Contact = contact,
|
||||
Account = account,
|
||||
RecentCases = cases,
|
||||
CandidateCount = 1
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ContactEntity?> SafeGetContactAsync(string contactId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _client.GetContactAsync(contactId, ContactSelect, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (EspoCrmAuthorizationException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "MatchingService: candidate contact fetch failed for {Id}", contactId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Invalidate(string senderEmail)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(senderEmail)) return;
|
||||
|
||||
@@ -84,6 +84,10 @@ namespace MarcusLaw.OutlookAddin.UI.Converters
|
||||
["New"] = "חדש",
|
||||
["Assigned"] = "הוקצה",
|
||||
["Pending"] = "ממתין",
|
||||
["PendingHearing"] = "ממתין לדיון",
|
||||
["Pending Hearing"] = "ממתין לדיון",
|
||||
["PendingDecision"] = "ממתין להחלטה",
|
||||
["Pending Decision"] = "ממתין להחלטה",
|
||||
["In Progress"] = "בטיפול",
|
||||
["InProgress"] = "בטיפול",
|
||||
["Open"] = "פתוח",
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<Window x:Class="MarcusLaw.OutlookAddin.UI.Dialogs.SaveAttachmentsDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:MarcusLaw.OutlookAddin.UI.Converters"
|
||||
Title="שמור קבצים מצורפים ל-Klear"
|
||||
Height="560" Width="620"
|
||||
MinHeight="420" MinWidth="500"
|
||||
FlowDirection="RightToLeft"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="CanResize"
|
||||
Background="#F8FAFC"
|
||||
FontFamily="Segoe UI, David, Frank Ruehl, Arial">
|
||||
<Window.Resources>
|
||||
<conv:CaseStatusToHebrewConverter x:Key="CaseStatusHe" />
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
|
||||
<SolidColorBrush x:Key="Primary" Color="#2563EB" />
|
||||
<SolidColorBrush x:Key="PrimaryHover" Color="#1D4ED8" />
|
||||
<SolidColorBrush x:Key="Border" Color="#E2E8F0" />
|
||||
<SolidColorBrush x:Key="Muted" Color="#64748B" />
|
||||
<SolidColorBrush x:Key="CardBg" Color="White" />
|
||||
<SolidColorBrush x:Key="HoverBg" Color="#F1F5F9" />
|
||||
<SolidColorBrush x:Key="SelectedBg" Color="#DBEAFE" />
|
||||
|
||||
<Style x:Key="Card" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource CardBg}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource Primary}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="22,8" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="MinWidth" Value="110" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="5" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource PrimaryHover}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#94A3B8" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SecondaryButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="#0F172A" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="20,8" />
|
||||
<Setter Property="MinWidth" Value="110" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="5" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource HoverBg}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="16" x:Name="RootGrid">
|
||||
<Grid.Resources>
|
||||
<Style x:Key="CaseGrid" TargetType="DataGrid">
|
||||
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="GridLinesVisibility" Value="Horizontal" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="#E2E8F0" />
|
||||
<Setter Property="RowBackground" Value="White" />
|
||||
<Setter Property="AlternatingRowBackground" Value="#F8FAFC" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="CanUserAddRows" Value="False" />
|
||||
<Setter Property="CanUserDeleteRows" Value="False" />
|
||||
<Setter Property="CanUserResizeRows" Value="False" />
|
||||
<Setter Property="CanUserReorderColumns" Value="False" />
|
||||
<Setter Property="SelectionMode" Value="Single" />
|
||||
<Setter Property="SelectionUnit" Value="FullRow" />
|
||||
<Setter Property="IsReadOnly" Value="True" />
|
||||
<Setter Property="RowHeight" Value="30" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Search -->
|
||||
<Border Grid.Row="0" Style="{StaticResource Card}" Padding="10,8" Margin="0,0,0,8">
|
||||
<DockPanel LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Left" Text="🔍" VerticalAlignment="Center" FontSize="16" Margin="0,0,8,0" />
|
||||
<TextBox x:Name="SearchBox"
|
||||
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Cases -->
|
||||
<Border Grid.Row="1" Style="{StaticResource Card}">
|
||||
<DataGrid ItemsSource="{Binding Cases}"
|
||||
SelectedItem="{Binding SelectedCase}"
|
||||
Style="{StaticResource CaseGrid}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="מס׳ תיק" Binding="{Binding Number}" Width="80" />
|
||||
<DataGridTextColumn Header="שם" Binding="{Binding Name}" Width="*" />
|
||||
<DataGridTextColumn Header="מס׳ בבית משפט" Binding="{Binding CourtCaseNumber}" Width="140" />
|
||||
<DataGridTextColumn Header="סטטוס" Binding="{Binding Status, Converter={StaticResource CaseStatusHe}}" Width="110" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Border>
|
||||
|
||||
<!-- Folder -->
|
||||
<DockPanel Grid.Row="2" Margin="0,10,0,0" LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Left" Text="תיקייה:" VerticalAlignment="Center"
|
||||
Margin="0,0,8,0" Foreground="{StaticResource Muted}" FontSize="12" />
|
||||
<TextBlock DockPanel.Dock="Right" Text="טוען…" VerticalAlignment="Center"
|
||||
Margin="8,0,0,0" Foreground="{StaticResource Muted}" FontSize="12"
|
||||
Visibility="{Binding IsLoadingSubfolders, Converter={StaticResource BoolToVis}}" />
|
||||
<ComboBox ItemsSource="{Binding Subfolders}"
|
||||
SelectedItem="{Binding SelectedSubfolder}"
|
||||
Padding="6,4">
|
||||
<ComboBox.Style>
|
||||
<Style TargetType="ComboBox">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsLoadingSubfolders}" Value="True">
|
||||
<Setter Property="IsEnabled" Value="False" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ComboBox.Style>
|
||||
</ComboBox>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Attachments header -->
|
||||
<TextBlock Grid.Row="3" Margin="0,12,0,4" Foreground="{StaticResource Muted}" FontSize="12">
|
||||
<Run Text="קבצים מצורפים (" /><Run Text="{Binding Attachments.Count, Mode=OneWay}" /><Run Text=")" />
|
||||
</TextBlock>
|
||||
|
||||
<!-- Attachments list -->
|
||||
<Border Grid.Row="4" Style="{StaticResource Card}" MaxHeight="160">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding Attachments}" Margin="6">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel Margin="0,3" LastChildFill="True">
|
||||
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding SizeDisplay}"
|
||||
DockPanel.Dock="Left"
|
||||
Foreground="{StaticResource Muted}"
|
||||
FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8,0,0,0" />
|
||||
<TextBox Text="{Binding FileName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding IsSelected}"
|
||||
VerticalAlignment="Center"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{StaticResource Border}"
|
||||
Padding="6,3"
|
||||
Margin="8,0,0,0"
|
||||
ToolTip="לחץ כדי לערוך את שם הקובץ לפני השמירה" />
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Action row -->
|
||||
<DockPanel Grid.Row="5" Margin="0,12,0,0" LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
Text="{Binding StatusMessage}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Foreground="{StaticResource Muted}"
|
||||
FontSize="12"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Button Command="{Binding SubmitCommand}"
|
||||
IsDefault="True"
|
||||
Content="שמור"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
Margin="0,0,8,0" />
|
||||
<Button Command="{Binding CancelCommand}"
|
||||
IsCancel="True"
|
||||
Content="ביטול"
|
||||
Style="{StaticResource SecondaryButton}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using MarcusLaw.OutlookAddin.UI.ViewModels;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.UI.Dialogs
|
||||
{
|
||||
public partial class SaveAttachmentsDialog : Window
|
||||
{
|
||||
private SaveAttachmentsViewModel? _viewModel;
|
||||
|
||||
public SaveAttachmentsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, __) => SearchBox.Focus();
|
||||
}
|
||||
|
||||
public SaveAttachmentsDialog(SaveAttachmentsViewModel viewModel) : this()
|
||||
{
|
||||
AttachViewModel(viewModel);
|
||||
}
|
||||
|
||||
public void AttachViewModel(SaveAttachmentsViewModel viewModel)
|
||||
{
|
||||
if (_viewModel != null) _viewModel.RequestClose -= OnRequestClose;
|
||||
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
|
||||
DataContext = _viewModel;
|
||||
_viewModel.RequestClose += OnRequestClose;
|
||||
}
|
||||
|
||||
private void OnRequestClose(object? sender, EventArgs e)
|
||||
{
|
||||
DialogResult = _viewModel?.DialogResult;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,18 @@
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Text="Marcus-Law OutlookAddin" FontWeight="Bold" FontSize="16" />
|
||||
<TextBlock Text="תוסף Klear ל-Microsoft Outlook" Margin="0,4,0,12" />
|
||||
<TextBlock Text="{Binding VersionLine}" Foreground="Gray" />
|
||||
<DockPanel LastChildFill="False" Margin="0,0,0,4">
|
||||
<TextBlock DockPanel.Dock="Left" Text="{Binding VersionLine}" Foreground="Gray" VerticalAlignment="Center" />
|
||||
<Button DockPanel.Dock="Right"
|
||||
Command="{Binding CheckForUpdateCommand}"
|
||||
Content="בדוק עדכונים"
|
||||
Padding="14,4"
|
||||
MinWidth="120" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="{Binding UpdateStatus}"
|
||||
Foreground="{Binding UpdateStatusBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,4,0,0" />
|
||||
<TextBlock Margin="0,16,0,0" TextWrapping="Wrap"
|
||||
Text="לוגים: %LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs" />
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Deployment" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using MarcusLaw.OutlookAddin.Core.Models;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
{
|
||||
public sealed partial class AttachmentRowViewModel : ObservableObject
|
||||
{
|
||||
public EspoAttachment Data { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSelected = true;
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get => Data.Name;
|
||||
set
|
||||
{
|
||||
var newName = value ?? string.Empty;
|
||||
if (Data.Name == newName) return;
|
||||
Data.Name = newName;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string SizeDisplay { get; }
|
||||
|
||||
public AttachmentRowViewModel(EspoAttachment data)
|
||||
{
|
||||
Data = data;
|
||||
SizeDisplay = ComputeSize(data.ContentBase64);
|
||||
}
|
||||
|
||||
private static string ComputeSize(string base64)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64)) return string.Empty;
|
||||
// Base64 expands binary 4-for-3, ignoring padding chars.
|
||||
var padding = 0;
|
||||
if (base64.Length > 0 && base64[base64.Length - 1] == '=') padding++;
|
||||
if (base64.Length > 1 && base64[base64.Length - 2] == '=') padding++;
|
||||
var bytes = (long)((base64.Length * 3) / 4) - padding;
|
||||
return FormatBytes(bytes);
|
||||
}
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024.0).ToString("F1") + " KB";
|
||||
return (bytes / (1024.0 * 1024.0)).ToString("F1") + " MB";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,9 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
private readonly IMatchingService _matchingService;
|
||||
private readonly ILogger _logger;
|
||||
private CancellationTokenSource? _activeLookup;
|
||||
// Stashes the ambiguous MatchResult while the user is drilling into
|
||||
// one of its candidates, so the "→ חזור" button can restore it.
|
||||
private MatchResult? _ambiguousReturnTarget;
|
||||
|
||||
[ObservableProperty]
|
||||
private ReadingPaneState state = ReadingPaneState.Empty;
|
||||
@@ -51,6 +54,8 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
|
||||
public ObservableCollection<CaseEntity> RecentCases { get; } = new ObservableCollection<CaseEntity>();
|
||||
|
||||
public ObservableCollection<ContactEntity> Candidates { get; } = new ObservableCollection<ContactEntity>();
|
||||
|
||||
public Visibility EmptyVisibility => Vis(State == ReadingPaneState.Empty);
|
||||
public Visibility LoadingVisibility => Vis(State == ReadingPaneState.Loading);
|
||||
public Visibility MatchVisibility => Vis(State == ReadingPaneState.Match);
|
||||
@@ -61,6 +66,22 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
public Visibility NoCasesVisibility => Vis(State == ReadingPaneState.Match && RecentCases.Count == 0);
|
||||
public Visibility ActionsVisibility => Vis(State == ReadingPaneState.Match);
|
||||
|
||||
/// <summary>
|
||||
/// True when the matched contact has 2+ linked cases. The sidebar
|
||||
/// uses this to hide the bottom "תייק לתיק הזה" button (which is
|
||||
/// ambiguous in that scenario) and to surface the per-row "תייק"
|
||||
/// buttons that let the user pick which case to file to.
|
||||
/// </summary>
|
||||
public Visibility MultiCaseHintVisibility => Vis(State == ReadingPaneState.Match && RecentCases.Count >= 2);
|
||||
public Visibility SingleCaseFileVisibility => Vis(State == ReadingPaneState.Match && RecentCases.Count == 1);
|
||||
|
||||
/// <summary>
|
||||
/// "→ חזור לרשימת אנשי קשר" is only shown when the user drilled
|
||||
/// into a candidate from the ambiguous picker; not in a normal
|
||||
/// single-match lookup.
|
||||
/// </summary>
|
||||
public Visibility BackToCandidatesVisibility => Vis(State == ReadingPaneState.Match && _ambiguousReturnTarget != null);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user clicks "File to this case" (carries the chosen
|
||||
/// MatchResult.Contact / RecentCases pick) or "File elsewhere" (null).
|
||||
@@ -89,6 +110,9 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
OnPropertyChanged(nameof(ErrorVisibility));
|
||||
OnPropertyChanged(nameof(ActionsVisibility));
|
||||
OnPropertyChanged(nameof(NoCasesVisibility));
|
||||
OnPropertyChanged(nameof(MultiCaseHintVisibility));
|
||||
OnPropertyChanged(nameof(SingleCaseFileVisibility));
|
||||
OnPropertyChanged(nameof(BackToCandidatesVisibility));
|
||||
FileToThisCommand.NotifyCanExecuteChanged();
|
||||
FileElsewhereCommand.NotifyCanExecuteChanged();
|
||||
OpenInEspoCrmCommand.NotifyCanExecuteChanged();
|
||||
@@ -101,8 +125,16 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
{
|
||||
foreach (var c in value.RecentCases) RecentCases.Add(c);
|
||||
}
|
||||
Candidates.Clear();
|
||||
if (value?.Candidates != null)
|
||||
{
|
||||
foreach (var c in value.Candidates) Candidates.Add(c);
|
||||
}
|
||||
OnPropertyChanged(nameof(AccountVisibility));
|
||||
OnPropertyChanged(nameof(NoCasesVisibility));
|
||||
OnPropertyChanged(nameof(MultiCaseHintVisibility));
|
||||
OnPropertyChanged(nameof(SingleCaseFileVisibility));
|
||||
OnPropertyChanged(nameof(BackToCandidatesVisibility));
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
@@ -112,6 +144,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
Match = null;
|
||||
ErrorMessage = null;
|
||||
AmbiguousMessage = null;
|
||||
_ambiguousReturnTarget = null;
|
||||
State = ReadingPaneState.Empty;
|
||||
}
|
||||
|
||||
@@ -125,6 +158,8 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
}
|
||||
|
||||
SenderEmail = senderEmail;
|
||||
// New email selection — drop any stashed candidate-list state.
|
||||
_ambiguousReturnTarget = null;
|
||||
State = ReadingPaneState.Loading;
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
@@ -182,7 +217,28 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
FileRequested?.Invoke(this, new FileFromSidebarEventArgs(Match!));
|
||||
}
|
||||
|
||||
private bool CanFileToThis() => State == ReadingPaneState.Match && Match?.Contact != null;
|
||||
// Bottom "תייק לתיק הזה" only makes sense with exactly one linked
|
||||
// case. 0 cases → nothing to file to; 2+ cases → ambiguous, the
|
||||
// per-row "תייק" buttons handle the pick.
|
||||
private bool CanFileToThis() => State == ReadingPaneState.Match && Match?.Contact != null && RecentCases.Count == 1;
|
||||
|
||||
[RelayCommand]
|
||||
private void FileToCase(CaseEntity? caseEntity)
|
||||
{
|
||||
if (caseEntity == null || string.IsNullOrWhiteSpace(caseEntity.Id) || Match == null) return;
|
||||
// Build a synthetic MatchResult so the host's
|
||||
// TryResolveSidebarAutoTarget path sees exactly one case and
|
||||
// auto-files without re-opening the search dialog.
|
||||
var synthetic = new MatchResult
|
||||
{
|
||||
SenderEmail = Match.SenderEmail,
|
||||
Contact = Match.Contact,
|
||||
Account = Match.Account,
|
||||
RecentCases = new System.Collections.Generic.List<CaseEntity> { caseEntity },
|
||||
CandidateCount = 1
|
||||
};
|
||||
FileRequested?.Invoke(this, new FileFromSidebarEventArgs(synthetic));
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanFileElsewhere))]
|
||||
private void FileElsewhere()
|
||||
@@ -202,12 +258,88 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
|
||||
private bool CanOpenInCrm() => State == ReadingPaneState.Match && Match?.Contact != null;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenCaseInCrm(CaseEntity? caseEntity)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EspoCrmBaseUrl) || caseEntity == null || string.IsNullOrWhiteSpace(caseEntity.Id)) return;
|
||||
var url = EspoCrmBaseUrl!.TrimEnd('/') + "/#Case/view/" + caseEntity.Id;
|
||||
OpenInBrowserRequested?.Invoke(this, url);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenManualSearch()
|
||||
{
|
||||
FileRequested?.Invoke(this, new FileFromSidebarEventArgs(null));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void BackToCandidates()
|
||||
{
|
||||
if (_ambiguousReturnTarget == null) return;
|
||||
var stash = _ambiguousReturnTarget;
|
||||
// Clear stash before we re-enter Ambiguous so visibility refresh
|
||||
// (triggered by OnMatchChanged) sees the cleared state and hides
|
||||
// the back button until the user drills in again.
|
||||
_ambiguousReturnTarget = null;
|
||||
Match = stash;
|
||||
AmbiguousMessage = $"נמצאו {stash.CandidateCount} אנשי קשר עם הכתובת הזו.";
|
||||
State = ReadingPaneState.Ambiguous;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User picked one of the ambiguous candidates from the sidebar.
|
||||
/// Load that contact's full bundle (account + cases) and transition
|
||||
/// to the Match state so the user can pick a case or file.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task SelectCandidateAsync(ContactEntity? candidate)
|
||||
{
|
||||
if (candidate == null || string.IsNullOrWhiteSpace(candidate.Id)) return;
|
||||
|
||||
CancelInFlight();
|
||||
var cts = new CancellationTokenSource();
|
||||
_activeLookup = cts;
|
||||
|
||||
var rememberedEmail = SenderEmail;
|
||||
// Stash the ambiguous result so "→ חזור" can restore it.
|
||||
if (Match?.IsAmbiguous == true)
|
||||
{
|
||||
_ambiguousReturnTarget = Match;
|
||||
}
|
||||
State = ReadingPaneState.Loading;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _matchingService.LookupContactAsync(candidate.Id, rememberedEmail, cts.Token).ConfigureAwait(true);
|
||||
if (cts.IsCancellationRequested) return;
|
||||
|
||||
if (result == null || result.Contact == null)
|
||||
{
|
||||
ErrorMessage = "טעינת איש הקשר נכשלה.";
|
||||
State = ReadingPaneState.Error;
|
||||
return;
|
||||
}
|
||||
|
||||
Match = result;
|
||||
State = ReadingPaneState.Match;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Selection changed before lookup finished — ignore.
|
||||
}
|
||||
catch (EspoCrmAuthorizationException)
|
||||
{
|
||||
ErrorMessage = "מפתח ה-API נדחה. עדכן בהגדרות.";
|
||||
State = ReadingPaneState.Error;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "ReadingPaneViewModel.SelectCandidateAsync failed for {ContactId}", candidate.Id);
|
||||
ErrorMessage = "שגיאת התחברות לשרת.";
|
||||
State = ReadingPaneState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private static Visibility Vis(bool show) => show ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MarcusLaw.OutlookAddin.Core.Models;
|
||||
using MarcusLaw.OutlookAddin.Core.Services;
|
||||
using Serilog;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Picks a Case + optional subfolder (filesystem-style, via Marcus-Law's
|
||||
/// NetworkStorageIntegration) for saving raw attachment files.
|
||||
/// </summary>
|
||||
public sealed partial class SaveAttachmentsViewModel : ObservableObject
|
||||
{
|
||||
// Default subfolders the integration creates under each Case folder.
|
||||
// Mirrors Resources/metadata/app/networkStorage.json on the server.
|
||||
private static readonly string[] DefaultCaseSubfolders =
|
||||
{
|
||||
"מסמכים",
|
||||
"אסמכתאות",
|
||||
"התכתבויות",
|
||||
"כללי"
|
||||
};
|
||||
|
||||
private const string RootSentinel = "(שורש התיק)";
|
||||
|
||||
private readonly IEspoCrmClient _client;
|
||||
private readonly ILogger _logger;
|
||||
private CancellationTokenSource? _searchCts;
|
||||
|
||||
[ObservableProperty]
|
||||
private string searchText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private CaseEntity? selectedCase;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? selectedSubfolder;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? statusMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isLoadingSubfolders;
|
||||
|
||||
/// <summary>
|
||||
/// Resolved server-side case folder path for the currently
|
||||
/// selected case (e.g. "47-אריאל מונאס"). Populated by
|
||||
/// <see cref="RefreshSubfoldersForSelectedCaseAsync"/> after the
|
||||
/// case + primary contact have been fetched. Empty until then.
|
||||
/// </summary>
|
||||
public string? ResolvedCaseFolderPath { get; private set; }
|
||||
|
||||
public ObservableCollection<CaseEntity> Cases { get; } = new ObservableCollection<CaseEntity>();
|
||||
|
||||
public ObservableCollection<string> Subfolders { get; } = new ObservableCollection<string>();
|
||||
|
||||
public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new ObservableCollection<AttachmentRowViewModel>();
|
||||
|
||||
public bool? DialogResult { get; private set; }
|
||||
|
||||
public IReadOnlyList<EspoAttachment> SelectedAttachments
|
||||
{
|
||||
get
|
||||
{
|
||||
var list = new List<EspoAttachment>();
|
||||
foreach (var row in Attachments)
|
||||
{
|
||||
if (row.IsSelected) list.Add(row.Data);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subfolder string the user picked, or null to save to the case
|
||||
/// folder root.
|
||||
/// </summary>
|
||||
public string? ChosenSubfolderName =>
|
||||
string.IsNullOrEmpty(SelectedSubfolder) ||
|
||||
string.Equals(SelectedSubfolder, RootSentinel, StringComparison.Ordinal)
|
||||
? null
|
||||
: SelectedSubfolder;
|
||||
|
||||
public CaseEntity? PickedCase => SelectedCase;
|
||||
|
||||
public event EventHandler? RequestClose;
|
||||
|
||||
public SaveAttachmentsViewModel(
|
||||
IEspoCrmClient client,
|
||||
ILogger logger,
|
||||
IEnumerable<EspoAttachment> attachments)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
ResetSubfolders();
|
||||
|
||||
foreach (var att in attachments)
|
||||
{
|
||||
Attachments.Add(new AttachmentRowViewModel(att));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSubfolders()
|
||||
{
|
||||
Subfolders.Clear();
|
||||
Subfolders.Add(RootSentinel);
|
||||
foreach (var f in DefaultCaseSubfolders) Subfolders.Add(f);
|
||||
SelectedSubfolder = Subfolders[0];
|
||||
}
|
||||
|
||||
partial void OnSearchTextChanged(string value)
|
||||
{
|
||||
_searchCts?.Cancel();
|
||||
_searchCts = new CancellationTokenSource();
|
||||
_ = SearchAsync(value, _searchCts.Token);
|
||||
}
|
||||
|
||||
partial void OnSelectedCaseChanged(CaseEntity? value)
|
||||
{
|
||||
SubmitCommand.NotifyCanExecuteChanged();
|
||||
_ = RefreshSubfoldersForSelectedCaseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the subfolder dropdown to merge static defaults with
|
||||
/// the actual folders that already exist on the network share for
|
||||
/// the chosen case. Also resolves <see cref="ResolvedCaseFolderPath"/>
|
||||
/// using the same logic as the server (primary contact's name, not
|
||||
/// the case's lawsuit title).
|
||||
/// </summary>
|
||||
public async Task RefreshSubfoldersForSelectedCaseAsync()
|
||||
{
|
||||
ResetSubfolders();
|
||||
ResolvedCaseFolderPath = null;
|
||||
|
||||
var c = SelectedCase;
|
||||
if (c == null || string.IsNullOrEmpty(c.Id)) return;
|
||||
|
||||
IsLoadingSubfolders = true;
|
||||
try
|
||||
{
|
||||
await RefreshSubfoldersCoreAsync(c).ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingSubfolders = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSubfoldersCoreAsync(CaseEntity c)
|
||||
{
|
||||
try { await _client.EnsureCaseSubfoldersAsync(c.Id).ConfigureAwait(true); }
|
||||
catch (Exception ex) { _logger.Warning(ex, "EnsureCaseSubfolders failed; subfolders may be missing on disk"); }
|
||||
|
||||
string? primaryContactName = null;
|
||||
try
|
||||
{
|
||||
var detail = await _client.GetCaseAsync(
|
||||
c.Id, "id,name,number,contactsIds,networkStorageFolderPath", default).ConfigureAwait(true);
|
||||
c.Number = detail.Number;
|
||||
c.Name = detail.Name;
|
||||
c.NetworkStorageFolderPath = detail.NetworkStorageFolderPath;
|
||||
c.ContactsIds = detail.ContactsIds ?? new List<string>();
|
||||
|
||||
if (c.ContactsIds.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contact = await _client.GetContactAsync(c.ContactsIds[0], "id,name", default).ConfigureAwait(true);
|
||||
primaryContactName = contact?.Name;
|
||||
}
|
||||
catch (Exception ex) { _logger.Warning(ex, "Primary contact fetch failed for case {CaseId}", c.Id); }
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _logger.Warning(ex, "GetCase for subfolder list failed"); }
|
||||
|
||||
var folderPath = ResolveCaseFolderPath(c, primaryContactName);
|
||||
ResolvedCaseFolderPath = folderPath;
|
||||
_logger.Information(
|
||||
"Resolved case folder for {CaseId} (number={Number}, contact={Contact}, stored={Stored}) → {Path}",
|
||||
c.Id, c.Number ?? "(none)", primaryContactName ?? "(none)", c.NetworkStorageFolderPath ?? "(none)", folderPath);
|
||||
if (string.IsNullOrEmpty(folderPath)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var items = await _client.ListNetworkStorageFolderAsync(folderPath).ConfigureAwait(true);
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!item.IsFolder) continue;
|
||||
if (Subfolders.Contains(item.Name)) continue;
|
||||
if (string.Equals(item.Name, RootSentinel, StringComparison.Ordinal)) continue;
|
||||
Subfolders.Add(item.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Listing subfolders for case {CaseId} failed", c.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors NetworkDocumentService::buildEntityFolderName for Case
|
||||
/// on the server side. Fallback order:
|
||||
/// 1) Case.networkStorageFolderPath (if the integration's hook
|
||||
/// has set it — most authoritative)
|
||||
/// 2) "<number>-<primary contact name>" ← server's recipe
|
||||
/// 3) "<number>-<case lawsuit name>"
|
||||
/// 4) contact / case name on its own
|
||||
/// </summary>
|
||||
public static string ResolveCaseFolderPath(CaseEntity c, string? primaryContactName = null)
|
||||
{
|
||||
if (c == null) return string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(c.NetworkStorageFolderPath))
|
||||
{
|
||||
return c.NetworkStorageFolderPath!.TrimEnd('/');
|
||||
}
|
||||
|
||||
var nameForFolder = !string.IsNullOrWhiteSpace(primaryContactName)
|
||||
? primaryContactName!
|
||||
: (c.Name ?? string.Empty);
|
||||
|
||||
var raw = !string.IsNullOrWhiteSpace(c.Number)
|
||||
? $"{c.Number}-{nameForFolder}"
|
||||
: nameForFolder;
|
||||
return SanitizeFolderSegment(raw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors LocalFilesystemClient::sanitizeFileName on the server side
|
||||
/// so client-computed paths match what the integration writes.
|
||||
/// </summary>
|
||||
public static string SanitizeFolderSegment(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) return string.Empty;
|
||||
foreach (var c in new[] { '׳', '״', '\'', '"', '‘', '’', '“', '”' })
|
||||
{
|
||||
name = name.Replace(c.ToString(), string.Empty);
|
||||
}
|
||||
foreach (var c in new[] { '/', '\\', ':', '*', '?', '<', '>', '|' })
|
||||
{
|
||||
name = name.Replace(c, '-');
|
||||
}
|
||||
while (name.Contains("--")) name = name.Replace("--", "-");
|
||||
while (name.Contains(" ")) name = name.Replace(" ", " ");
|
||||
name = name.Replace("-.", ".");
|
||||
return name.Trim(' ', '-');
|
||||
}
|
||||
|
||||
private async Task<CaseEntity?> LoadCaseRowAsync(EspoEntityRef hit, CancellationToken ct)
|
||||
{
|
||||
var row = new CaseEntity { Id = hit.Id, Name = hit.Name };
|
||||
if (string.IsNullOrEmpty(hit.Id)) return row;
|
||||
try
|
||||
{
|
||||
var detail = await _client.GetCaseAsync(
|
||||
hit.Id, "id,name,number,status,cCourtCaseNumber", ct).ConfigureAwait(false);
|
||||
row.Number = detail.Number;
|
||||
row.Status = detail.Status;
|
||||
row.CourtCaseNumber = detail.CourtCaseNumber;
|
||||
if (!string.IsNullOrEmpty(detail.Name)) row.Name = detail.Name;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Hydrating case {CaseId} for picker failed", hit.Id);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private async Task SearchAsync(string query, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(250, ct).ConfigureAwait(true);
|
||||
|
||||
var trimmed = (query ?? string.Empty).Trim();
|
||||
if (trimmed.Length < 2)
|
||||
{
|
||||
Cases.Clear();
|
||||
StatusMessage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var search = await _client.GlobalSearchAsync(trimmed, ct).ConfigureAwait(true);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Cases.Clear();
|
||||
var caseHits = new List<EspoEntityRef>();
|
||||
foreach (var hit in search.List)
|
||||
{
|
||||
if (string.Equals(hit.EntityType, "Case", StringComparison.OrdinalIgnoreCase))
|
||||
caseHits.Add(hit);
|
||||
}
|
||||
|
||||
if (caseHits.Count == 0)
|
||||
{
|
||||
StatusMessage = "אין תוצאות מסוג תיק (Case)";
|
||||
return;
|
||||
}
|
||||
|
||||
var detailTasks = new List<Task<CaseEntity?>>(caseHits.Count);
|
||||
foreach (var hit in caseHits)
|
||||
{
|
||||
detailTasks.Add(LoadCaseRowAsync(hit, ct));
|
||||
}
|
||||
var details = await Task.WhenAll(detailTasks).ConfigureAwait(true);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var row in details)
|
||||
{
|
||||
if (row != null) Cases.Add(row);
|
||||
}
|
||||
StatusMessage = $"נמצאו {Cases.Count} תיקים";
|
||||
}
|
||||
catch (OperationCanceledException) { /* superseded */ }
|
||||
catch (EspoCrmAuthorizationException)
|
||||
{
|
||||
StatusMessage = "מפתח ה-API נדחה — יש לעדכן בהגדרות";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "SaveAttachments search failed");
|
||||
StatusMessage = "שגיאת חיפוש: " + ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSubmit))]
|
||||
private void Submit()
|
||||
{
|
||||
DialogResult = true;
|
||||
RequestClose?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private bool CanSubmit()
|
||||
{
|
||||
if (SelectedCase == null || string.IsNullOrWhiteSpace(SelectedCase.Id)) return false;
|
||||
foreach (var row in Attachments)
|
||||
{
|
||||
if (row.IsSelected) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
DialogResult = false;
|
||||
RequestClose?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,15 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
[ObservableProperty]
|
||||
private Brush statusBrush = NeutralBrush;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? updateStatus;
|
||||
|
||||
[ObservableProperty]
|
||||
private Brush updateStatusBrush = NeutralBrush;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCheckingForUpdate;
|
||||
|
||||
public string InitialApiKey { get; }
|
||||
|
||||
public string VersionLine { get; }
|
||||
@@ -70,8 +79,56 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
Username = creds?.Username ?? string.Empty;
|
||||
InitialApiKey = creds?.ApiKey ?? string.Empty;
|
||||
|
||||
var version = typeof(SettingsViewModel).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
VersionLine = $"גרסה {version}";
|
||||
VersionLine = $"גרסה {ResolveDisplayVersion()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks the most accurate version to surface in the About tab:
|
||||
/// the ClickOnce activation version when the add-in is installed
|
||||
/// over the wire (auto-updated to the real deployment version),
|
||||
/// else the assembly's AssemblyVersion (which is set per-source
|
||||
/// and may lag behind the deployed ClickOnce manifest).
|
||||
/// </summary>
|
||||
private static string ResolveDisplayVersion()
|
||||
{
|
||||
Version? v = null;
|
||||
try
|
||||
{
|
||||
if (System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed)
|
||||
{
|
||||
v = System.Deployment.Application.ApplicationDeployment
|
||||
.CurrentDeployment.CurrentVersion;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Not running under ClickOnce (dev / F5), or System.Deployment
|
||||
// is not available — fall through to the AssemblyVersion.
|
||||
}
|
||||
// The host VSTO assembly is where versioning policy lives
|
||||
// (csproj + AssemblyInfo). The UI assembly is SDK-style with
|
||||
// no <Version>, so it always reports 1.0.0.0 — don't fall back
|
||||
// to it. Walk loaded assemblies for the host by name suffix.
|
||||
if (v == null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var name = asm.GetName().Name;
|
||||
if (name != null && name.EndsWith(".OutlookAddin", StringComparison.Ordinal))
|
||||
{
|
||||
v = asm.GetName().Version;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
v ??= typeof(SettingsViewModel).Assembly.GetName().Version;
|
||||
if (v == null) return "1.0.0";
|
||||
|
||||
// ClickOnce stores 4 components; project policy is MAJOR.MINOR.PATCH —
|
||||
// hide the trailing revision when it's 0.
|
||||
return v.Revision == 0
|
||||
? $"{v.Major}.{v.Minor}.{v.Build}"
|
||||
: v.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,6 +162,66 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
|
||||
private static string WatchedKey(string storeId, string path) => storeId + "|" + path;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanCheckForUpdate))]
|
||||
private async Task CheckForUpdateAsync()
|
||||
{
|
||||
IsCheckingForUpdate = true;
|
||||
UpdateStatusBrush = NeutralBrush;
|
||||
UpdateStatus = "בודק עדכונים…";
|
||||
try
|
||||
{
|
||||
if (!System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed)
|
||||
{
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = "התוסף לא מותקן דרך ClickOnce — אין כאן עדכון אוטומטי.";
|
||||
return;
|
||||
}
|
||||
|
||||
var deployment = System.Deployment.Application.ApplicationDeployment.CurrentDeployment;
|
||||
var info = await Task.Run(() => deployment.CheckForDetailedUpdate(false)).ConfigureAwait(true);
|
||||
if (!info.UpdateAvailable)
|
||||
{
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = "אתה על הגרסה האחרונה (" + deployment.CurrentVersion + ").";
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStatus = $"זמינה גרסה {info.AvailableVersion} — מוריד…";
|
||||
var result = await Task.Run(() => deployment.Update()).ConfigureAwait(true);
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = $"הותקנה גרסה {info.AvailableVersion}. סגור ופתח את Outlook כדי לטעון אותה.";
|
||||
}
|
||||
catch (System.Deployment.Application.DeploymentDownloadException ex)
|
||||
{
|
||||
_logger.Warning(ex, "CheckForUpdate: download failed");
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = "הורדת העדכון נכשלה: " + ex.Message;
|
||||
}
|
||||
catch (System.Deployment.Application.InvalidDeploymentException ex)
|
||||
{
|
||||
_logger.Warning(ex, "CheckForUpdate: invalid deployment manifest");
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = "ה-deployment manifest על השרת לא תקין: " + ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "CheckForUpdate failed");
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = "שגיאה בבדיקת עדכונים: " + ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsCheckingForUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanCheckForUpdate() => !IsCheckingForUpdate;
|
||||
|
||||
partial void OnIsCheckingForUpdateChanged(bool value)
|
||||
{
|
||||
CheckForUpdateCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
|
||||
@@ -62,6 +62,29 @@
|
||||
<!-- Ambiguous -->
|
||||
<StackPanel Visibility="{Binding AmbiguousVisibility}">
|
||||
<TextBlock Text="{Binding AmbiguousMessage}" Foreground="DarkOrange" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="בחר איש קשר:" Margin="0,8,0,2" />
|
||||
<ItemsControl ItemsSource="{Binding Candidates}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#EEE" BorderThickness="0,0,0,1" Padding="0,4">
|
||||
<Button Command="{Binding DataContext.SelectCandidateCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
HorizontalContentAlignment="Right"
|
||||
HorizontalAlignment="Stretch">
|
||||
<StackPanel HorizontalAlignment="Stretch">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" Foreground="#0066CC" />
|
||||
<TextBlock Text="{Binding AccountName}" Foreground="DimGray" FontSize="11" />
|
||||
<TextBlock Text="{Binding EmailAddress}" Foreground="Gray" FontSize="10" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Content="פתח חיפוש ידני"
|
||||
Command="{Binding OpenManualSearchCommand}"
|
||||
Style="{StaticResource LinkButtonStyle}"
|
||||
@@ -70,8 +93,20 @@
|
||||
|
||||
<!-- Match -->
|
||||
<StackPanel Visibility="{Binding MatchVisibility}">
|
||||
<Button Content="→ חזור לרשימת אנשי קשר"
|
||||
Command="{Binding BackToCandidatesCommand}"
|
||||
Style="{StaticResource LinkButtonStyle}"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding BackToCandidatesVisibility}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="איש קשר" />
|
||||
<TextBlock Text="{Binding Match.Contact.Name}" FontSize="14" FontWeight="Bold" />
|
||||
<TextBlock FontSize="14" FontWeight="Bold">
|
||||
<Hyperlink Command="{Binding OpenInEspoCrmCommand}"
|
||||
TextDecorations="{x:Null}"
|
||||
Foreground="#0066CC">
|
||||
<Run Text="{Binding Match.Contact.Name, Mode=OneWay}" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Match.Contact.EmailAddress}" Foreground="Gray" />
|
||||
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="לקוח/חברה" Visibility="{Binding AccountVisibility}" />
|
||||
@@ -82,14 +117,33 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#EEE" BorderThickness="0,0,0,1" Padding="0,4">
|
||||
<StackPanel>
|
||||
<TextBlock>
|
||||
<Run Text="["/><Run Text="{Binding Number, Mode=OneWay}"/><Run Text="] " />
|
||||
<Run Text="{Binding Name, Mode=OneWay}" FontWeight="SemiBold" />
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Status, Converter={StaticResource HebrewStatus}}"
|
||||
Foreground="Gray" FontSize="10" />
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock>
|
||||
<Hyperlink Command="{Binding DataContext.OpenCaseInCrmCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
TextDecorations="{x:Null}"
|
||||
Foreground="#0066CC">
|
||||
<Run Text="["/><Run Text="{Binding Number, Mode=OneWay}"/><Run Text="] " />
|
||||
<Run Text="{Binding Name, Mode=OneWay}" FontWeight="SemiBold" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Status, Converter={StaticResource HebrewStatus}}"
|
||||
Foreground="Gray" FontSize="10" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Content="תייק"
|
||||
Command="{Binding DataContext.FileToCaseCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
Padding="8,2"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Top"
|
||||
ToolTip="תייק את המייל הנוכחי לתיק הזה" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -110,7 +164,12 @@
|
||||
<Button Content="תייק לתיק הזה"
|
||||
Command="{Binding FileToThisCommand}"
|
||||
Padding="8,4" Margin="0,0,0,4"
|
||||
HorizontalAlignment="Stretch" />
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{Binding SingleCaseFileVisibility}" />
|
||||
<TextBlock Text="יש כמה תיקים מקושרים — בחר תיק מהרשימה למעלה."
|
||||
Foreground="Gray" FontStyle="Italic" FontSize="10"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding MultiCaseHintVisibility}" />
|
||||
<Button Content="תייק למקום אחר…"
|
||||
Command="{Binding FileElsewhereCommand}"
|
||||
Padding="8,4" Margin="0,0,0,4"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 647 B |
@@ -31,13 +31,13 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<DefineConstants>VSTO40;UseOfficeInterop</DefineConstants>
|
||||
<ResolveComReferenceSilent>true</ResolveComReferenceSilent>
|
||||
<IsWebBootstrapper>False</IsWebBootstrapper>
|
||||
<IsWebBootstrapper>True</IsWebBootstrapper>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
<PublishUrl>C:\Users\Chaim\source\repos\OutlookAddin\Publish\</PublishUrl>
|
||||
<InstallUrl>\\192.168.10.97\Projects\LegalCRM\Add-in\</InstallUrl>
|
||||
<InstallUrl>https://gitea.dev.marcus-law.co.il/espocrm-extensions/OutlookAddin/raw/branch/main/Publish/</InstallUrl>
|
||||
<TargetCulture>en</TargetCulture>
|
||||
<ApplicationVersion>1.0.0.4</ApplicationVersion>
|
||||
<AutoIncrementApplicationRevision>true</AutoIncrementApplicationRevision>
|
||||
<ApplicationVersion>1.1.2.0</ApplicationVersion>
|
||||
<AutoIncrementApplicationRevision>false</AutoIncrementApplicationRevision>
|
||||
<UpdateEnabled>true</UpdateEnabled>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
<UpdateIntervalUnits>days</UpdateIntervalUnits>
|
||||
@@ -270,6 +270,9 @@
|
||||
<EmbeddedResource Include="Images\klear-compose.png">
|
||||
<LogicalName>OutlookAddin.Images.klear-compose.png</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Images\klear-attach.png">
|
||||
<LogicalName>OutlookAddin.Images.klear-attach.png</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<Compile Include="Services\AddInHost.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
|
||||
@@ -33,6 +33,6 @@ using System.Security;
|
||||
// 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")]
|
||||
[assembly: AssemblyVersion("1.1.2.0")]
|
||||
[assembly: AssemblyFileVersion("1.1.2.0")]
|
||||
|
||||
|
||||
@@ -19,7 +19,15 @@ namespace OutlookAddin.Ribbon
|
||||
{
|
||||
private const string ExplorerResource = "OutlookAddin.Ribbon.ExplorerRibbon.xml";
|
||||
private const string InspectorResource = "OutlookAddin.Ribbon.InspectorRibbon.xml";
|
||||
private const string FileButtonId = "FileToEspoCrm";
|
||||
|
||||
// Every button whose `getEnabled` depends on the current Explorer
|
||||
// selection must be re-evaluated on every SelectionChange — Office
|
||||
// caches the initial result otherwise and the button stays stuck.
|
||||
private static readonly string[] SelectionDependentButtons =
|
||||
{
|
||||
"FileToEspoCrm",
|
||||
"SaveAttachments"
|
||||
};
|
||||
|
||||
private IRibbonUI? _explorerRibbon;
|
||||
private IRibbonUI? _inspectorRibbon;
|
||||
@@ -45,7 +53,7 @@ namespace OutlookAddin.Ribbon
|
||||
var explorer = Globals.ThisAddIn.Application.ActiveExplorer();
|
||||
if (explorer != null)
|
||||
{
|
||||
explorer.SelectionChange += () => _explorerRibbon?.InvalidateControl(FileButtonId);
|
||||
explorer.SelectionChange += InvalidateSelectionDependentButtons;
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -54,6 +62,16 @@ namespace OutlookAddin.Ribbon
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateSelectionDependentButtons()
|
||||
{
|
||||
var ribbon = _explorerRibbon;
|
||||
if (ribbon == null) return;
|
||||
foreach (var id in SelectionDependentButtons)
|
||||
{
|
||||
try { ribbon.InvalidateControl(id); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInspectorRibbonLoad(IRibbonUI ribbon)
|
||||
{
|
||||
_inspectorRibbon = ribbon;
|
||||
@@ -92,6 +110,43 @@ namespace OutlookAddin.Ribbon
|
||||
Globals.ThisAddIn.AddInHost.ShowSidebar();
|
||||
}
|
||||
|
||||
public bool OnGetSaveAttachmentsEnabled(IRibbonControl control)
|
||||
{
|
||||
// Enabled only when EXACTLY one MailItem is selected AND it has
|
||||
// at least one attachment. Doing multi-mail save would conflict
|
||||
// with the per-file checkbox UX in the dialog.
|
||||
try
|
||||
{
|
||||
var selection = Globals.ThisAddIn.Application.ActiveExplorer()?.Selection;
|
||||
if (selection == null || selection.Count != 1) return false;
|
||||
if (selection[1] is not Outlook.MailItem mail) return false;
|
||||
var attachments = mail.Attachments;
|
||||
if (attachments == null) return false;
|
||||
return attachments.Count > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSaveAttachments(IRibbonControl control)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selection = Globals.ThisAddIn.Application.ActiveExplorer()?.Selection;
|
||||
if (selection == null || selection.Count != 1) return;
|
||||
if (selection[1] is Outlook.MailItem mail)
|
||||
{
|
||||
_ = Globals.ThisAddIn.AddInHost.LaunchSaveAttachmentsAsync(mail);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Globals.ThisAddIn.AddInHost.Logger.Warning(ex, "OnSaveAttachments failed");
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnGetComposeFromCaseVisible(IRibbonControl control)
|
||||
{
|
||||
// Show only on outgoing (compose) mail inspectors.
|
||||
@@ -150,6 +205,7 @@ namespace OutlookAddin.Ribbon
|
||||
switch (control.Id)
|
||||
{
|
||||
case "FileToEspoCrm": fileName = "klear-file.png"; break;
|
||||
case "SaveAttachments": fileName = "klear-attach.png"; break;
|
||||
case "ShowSidebar": fileName = "klear-sidebar.png"; break;
|
||||
case "OpenEspoCrmSettings": fileName = "klear-settings.png"; break;
|
||||
case "ComposeFromCase": fileName = "klear-compose.png"; break;
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
getImage="OnGetImage"
|
||||
onAction="OnFileToEspoCrm"
|
||||
getEnabled="OnGetFileToEspoCrmEnabled" />
|
||||
<button id="SaveAttachments"
|
||||
label="שמור קבצים"
|
||||
screentip="שמור את הקבצים המצורפים בלבד"
|
||||
supertip="מעלה את הקבצים המצורפים של המייל הנבחר ל-Klear כ-Documents מקושרים לתיק שתבחר, בלי לתייק את המייל עצמו."
|
||||
size="large"
|
||||
getImage="OnGetImage"
|
||||
onAction="OnSaveAttachments"
|
||||
getEnabled="OnGetSaveAttachmentsEnabled" />
|
||||
<button id="ShowSidebar"
|
||||
label="פרטי תיק"
|
||||
screentip="הצג את סרגל פרטי התיק"
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace OutlookAddin.Services
|
||||
public IEspoCrmClient? CrmClient { get; private set; }
|
||||
public IFilingService? FilingService { get; private set; }
|
||||
public IMatchingService? MatchingService { get; private set; }
|
||||
public IAttachmentSaveService? AttachmentSaveService { get; private set; }
|
||||
public ComposeService? ComposeService { get; private set; }
|
||||
|
||||
private readonly Outlook.Application _outlookApp;
|
||||
@@ -353,6 +354,7 @@ namespace OutlookAddin.Services
|
||||
FilingService = new FilingService(CrmClient, RetryQueue, Logger);
|
||||
MatchingService = new MatchingService(CrmClient, _matchCache, Logger);
|
||||
ComposeService = new ComposeService(CrmClient, Logger);
|
||||
AttachmentSaveService = new AttachmentSaveService(CrmClient, Logger);
|
||||
_folderResolver ??= new FolderResolver(_outlookApp, Logger);
|
||||
_folderWatcher = new FolderWatcher(
|
||||
_outlookApp,
|
||||
@@ -387,6 +389,7 @@ namespace OutlookAddin.Services
|
||||
FilingService = null;
|
||||
MatchingService = null;
|
||||
ComposeService = null;
|
||||
AttachmentSaveService = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -428,6 +431,136 @@ namespace OutlookAddin.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save-attachments-only flow. The user clicks the ribbon's "שמור
|
||||
/// קבצים" button while standing on a mail with one or more
|
||||
/// attachments. We extract the attachments (no Email entity
|
||||
/// created), let the user pick a Case + optional DocumentFolder,
|
||||
/// then upload each attachment + create a Document linked to the
|
||||
/// case.
|
||||
/// </summary>
|
||||
public async Task LaunchSaveAttachmentsAsync(Outlook.MailItem mail)
|
||||
{
|
||||
if (mail == null) return;
|
||||
if (!IsConfigured)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Information);
|
||||
if (result == MessageBoxResult.Yes) LaunchSettings();
|
||||
if (!IsConfigured) return;
|
||||
}
|
||||
|
||||
// Extract on the STA — MailItemExtractor handles all COM access
|
||||
// and gives us a self-contained envelope.
|
||||
MailEnvelope envelope;
|
||||
try
|
||||
{
|
||||
envelope = MailItemExtractor.Extract(mail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "LaunchSaveAttachmentsAsync: extract failed");
|
||||
MessageBox.Show(
|
||||
"קריאת הקבצים המצורפים נכשלה: " + ex.Message,
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelope.Attachments.Count == 0)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"אין במייל הזה קבצים מצורפים לשמירה.",
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var vm = new SaveAttachmentsViewModel(CrmClient!, Logger, envelope.Attachments);
|
||||
// Subfolders are populated lazily once the user picks a case;
|
||||
// the default static set is already pre-loaded.
|
||||
|
||||
var dialog = new SaveAttachmentsDialog(vm);
|
||||
AttachOwnerToOutlook(dialog);
|
||||
var ok = dialog.ShowDialog();
|
||||
if (ok != true) return;
|
||||
|
||||
var picked = vm.PickedCase;
|
||||
var selected = vm.SelectedAttachments;
|
||||
if (picked == null || string.IsNullOrEmpty(picked.Id) || selected.Count == 0) return;
|
||||
|
||||
// Prefer the path the VM resolved when the user picked the case
|
||||
// (it already fetched the primary contact and applied the
|
||||
// server's naming convention). Fall back to the no-contact
|
||||
// resolve only if that step somehow didn't run.
|
||||
var caseFolder = !string.IsNullOrWhiteSpace(vm.ResolvedCaseFolderPath)
|
||||
? vm.ResolvedCaseFolderPath!
|
||||
: SaveAttachmentsViewModel.ResolveCaseFolderPath(picked);
|
||||
if (string.IsNullOrEmpty(caseFolder))
|
||||
{
|
||||
MessageBox.Show(
|
||||
"לא ניתן לחשב את נתיב התיקייה של התיק. ייתכן שהתיק עוד לא הוגדר ב-Klear.",
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
var subfolder = vm.ChosenSubfolderName;
|
||||
var targetFolderPath = string.IsNullOrEmpty(subfolder)
|
||||
? caseFolder
|
||||
: caseFolder + "/" + subfolder;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Information(
|
||||
"Saving {Count} attachment(s) to Case/{Id} ({Name}) → {Path}",
|
||||
selected.Count, picked.Id, picked.Name, targetFolderPath);
|
||||
|
||||
// Safety net: ensure the case folder + defaults exist on
|
||||
// disk before the per-file uploads (the dialog may have
|
||||
// been submitted before the prior ensure call finished).
|
||||
try { await CrmClient!.EnsureCaseSubfoldersAsync(picked.Id).ConfigureAwait(true); }
|
||||
catch (Exception ex) { Logger.Warning(ex, "Pre-upload ensure failed; continuing anyway"); }
|
||||
|
||||
var summary = await AttachmentSaveService!.SaveAsync(selected, targetFolderPath).ConfigureAwait(true);
|
||||
ShowAttachmentSummary(summary, picked.Name ?? picked.Id, targetFolderPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "LaunchSaveAttachmentsAsync failed");
|
||||
MessageBox.Show(
|
||||
"שמירת הקבצים נכשלה: " + ex.Message,
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowAttachmentSummary(AttachmentSaveSummary summary, string caseLabel, string targetPath)
|
||||
{
|
||||
string message;
|
||||
MessageBoxImage icon;
|
||||
if (summary.AuthRequired)
|
||||
{
|
||||
message = "מפתח ה-API נדחה. יש לעדכן את ההגדרות ולנסות שוב.";
|
||||
icon = MessageBoxImage.Warning;
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (summary.SavedCount > 0) parts.Add($"נשמרו {summary.SavedCount} קבצים אל \"{caseLabel}\" ({targetPath})");
|
||||
if (summary.FailedCount > 0) parts.Add($"{summary.FailedCount} נכשלו");
|
||||
message = parts.Count == 0 ? "לא נשמרו פריטים." : string.Join(" · ", parts);
|
||||
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
|
||||
}
|
||||
MessageBox.Show(message, "Marcus-Law OutlookAddin", MessageBoxButton.OK, icon);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for the URL-protocol handler (outlookaddin://compose?caseId=…).
|
||||
/// Creates a brand new MailItem, populates it with case details, then
|
||||
@@ -510,7 +643,14 @@ namespace OutlookAddin.Services
|
||||
/// current Explorer selection, opens the search dialog, files on success,
|
||||
/// and stamps categories on the chosen MailItems.
|
||||
/// </summary>
|
||||
public async Task LaunchFileToCaseAsync()
|
||||
public Task LaunchFileToCaseAsync() => LaunchFileToCaseAsync(null);
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="LaunchFileToCaseAsync()"/>, but when the sidebar
|
||||
/// already resolved a single linked case for the sender we skip the
|
||||
/// search dialog and file directly to that case.
|
||||
/// </summary>
|
||||
public async Task LaunchFileToCaseAsync(MatchResult? preselectedMatch)
|
||||
{
|
||||
if (!IsConfigured)
|
||||
{
|
||||
@@ -540,39 +680,50 @@ namespace OutlookAddin.Services
|
||||
try
|
||||
{
|
||||
var envelopes = mailItems.Select(MailItemExtractor.Extract).ToList();
|
||||
var firstEnvelope = envelopes[0];
|
||||
|
||||
var vm = new FileToCaseViewModel(CrmClient!, Settings, Logger)
|
||||
FilingTarget? target = TryResolveSidebarAutoTarget(preselectedMatch);
|
||||
if (target != null)
|
||||
{
|
||||
SenderEmail = firstEnvelope.From,
|
||||
SenderName = TryGetSenderName(mailItems[0])
|
||||
};
|
||||
|
||||
var dialog = new FileToCaseDialog(vm);
|
||||
AttachOwnerToOutlook(dialog);
|
||||
var ok = dialog.ShowDialog();
|
||||
Logger.Information(
|
||||
"FileToCase dialog closed (ok={Ok}, selected id={Id}, entityType={Type})",
|
||||
ok, vm.SelectedTarget?.Id, vm.SelectedTarget?.EntityType);
|
||||
if (ok != true) return;
|
||||
|
||||
var target = vm.GetSelectedFilingTarget();
|
||||
if (target == null)
|
||||
{
|
||||
Logger.Warning(
|
||||
"Filing skipped — selected EspoEntityRef has no usable Id/EntityType (id={Id}, name={Name}, type={Type})",
|
||||
vm.SelectedTarget?.Id, vm.SelectedTarget?.Name, vm.SelectedTarget?.EntityType);
|
||||
MessageBox.Show(
|
||||
"התיק שנבחר לא הוחזר עם סוג ישות (entityType) תקין. ייתכן ש-Klear שלך מחזיר שדה אחר; הלוג מכיל את הפרטים. שלח לאדמין.",
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
return;
|
||||
Logger.Information(
|
||||
"Auto-filing {Count} mail item(s) from sidebar to {Type}/{Id} ({Name})",
|
||||
envelopes.Count, target.ParentType, target.ParentId, target.ParentName);
|
||||
}
|
||||
else
|
||||
{
|
||||
var firstEnvelope = envelopes[0];
|
||||
|
||||
Logger.Information(
|
||||
"Filing {Count} mail item(s) to {Type}/{Id} ({Name})",
|
||||
envelopes.Count, target.ParentType, target.ParentId, target.ParentName);
|
||||
var vm = new FileToCaseViewModel(CrmClient!, Settings, Logger)
|
||||
{
|
||||
SenderEmail = firstEnvelope.From,
|
||||
SenderName = TryGetSenderName(mailItems[0])
|
||||
};
|
||||
|
||||
var dialog = new FileToCaseDialog(vm);
|
||||
AttachOwnerToOutlook(dialog);
|
||||
var ok = dialog.ShowDialog();
|
||||
Logger.Information(
|
||||
"FileToCase dialog closed (ok={Ok}, selected id={Id}, entityType={Type})",
|
||||
ok, vm.SelectedTarget?.Id, vm.SelectedTarget?.EntityType);
|
||||
if (ok != true) return;
|
||||
|
||||
target = vm.GetSelectedFilingTarget();
|
||||
if (target == null)
|
||||
{
|
||||
Logger.Warning(
|
||||
"Filing skipped — selected EspoEntityRef has no usable Id/EntityType (id={Id}, name={Name}, type={Type})",
|
||||
vm.SelectedTarget?.Id, vm.SelectedTarget?.Name, vm.SelectedTarget?.EntityType);
|
||||
MessageBox.Show(
|
||||
"התיק שנבחר לא הוחזר עם סוג ישות (entityType) תקין. ייתכן ש-Klear שלך מחזיר שדה אחר; הלוג מכיל את הפרטים. שלח לאדמין.",
|
||||
"Marcus-Law OutlookAddin",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Information(
|
||||
"Filing {Count} mail item(s) to {Type}/{Id} ({Name})",
|
||||
envelopes.Count, target.ParentType, target.ParentId, target.ParentName);
|
||||
}
|
||||
|
||||
var summary = await FilingService!.FileAsync(envelopes, target).ConfigureAwait(true);
|
||||
|
||||
@@ -589,6 +740,21 @@ namespace OutlookAddin.Services
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar's "תייק לתיק הזה" makes a hard promise about which case
|
||||
// gets the mail — only honour it when the match resolved a single
|
||||
// linked case. With 0 or 2+ cases there is no unambiguous "this",
|
||||
// and the caller falls back to the manual search dialog.
|
||||
private static FilingTarget? TryResolveSidebarAutoTarget(MatchResult? match)
|
||||
{
|
||||
if (match?.RecentCases == null || match.RecentCases.Count != 1) return null;
|
||||
var c = match.RecentCases[0];
|
||||
if (string.IsNullOrWhiteSpace(c.Id)) return null;
|
||||
var label = !string.IsNullOrWhiteSpace(c.Number)
|
||||
? $"[{c.Number}] {c.Name}"
|
||||
: (c.Name ?? c.Id);
|
||||
return new FilingTarget("Case", c.Id, label);
|
||||
}
|
||||
|
||||
private List<Outlook.MailItem> ReadCurrentSelection()
|
||||
{
|
||||
var result = new List<Outlook.MailItem>();
|
||||
|
||||
@@ -98,9 +98,11 @@ namespace OutlookAddin.Services
|
||||
{
|
||||
att.SaveAsFile(tempPath);
|
||||
var bytes = File.ReadAllBytes(tempPath);
|
||||
var name = att.FileName ?? "attachment.bin";
|
||||
env.Attachments.Add(new EspoAttachment
|
||||
{
|
||||
Name = att.FileName ?? "attachment.bin",
|
||||
Name = name,
|
||||
ContentType = GuessMimeType(name),
|
||||
ContentBase64 = Convert.ToBase64String(bytes)
|
||||
});
|
||||
}
|
||||
@@ -171,6 +173,70 @@ namespace OutlookAddin.Services
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a sane MIME type from the file extension. EspoCRM's default
|
||||
/// attachment-upload config rejects "application/octet-stream" with
|
||||
/// "Not allowed file type", so we map common Office / image / archive
|
||||
/// extensions to canonical MIMEs before posting.
|
||||
/// </summary>
|
||||
private static string GuessMimeType(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName)) return "application/octet-stream";
|
||||
var ext = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
switch (ext)
|
||||
{
|
||||
// Office (legacy + OOXML)
|
||||
case ".pdf": return "application/pdf";
|
||||
case ".doc": return "application/msword";
|
||||
case ".docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
case ".xls": return "application/vnd.ms-excel";
|
||||
case ".xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
case ".ppt": return "application/vnd.ms-powerpoint";
|
||||
case ".pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
||||
case ".rtf": return "application/rtf";
|
||||
case ".odt": return "application/vnd.oasis.opendocument.text";
|
||||
case ".ods": return "application/vnd.oasis.opendocument.spreadsheet";
|
||||
|
||||
// Text
|
||||
case ".txt": return "text/plain";
|
||||
case ".csv": return "text/csv";
|
||||
case ".log": return "text/plain";
|
||||
case ".htm":
|
||||
case ".html": return "text/html";
|
||||
case ".xml": return "application/xml";
|
||||
case ".json": return "application/json";
|
||||
|
||||
// Images
|
||||
case ".png": return "image/png";
|
||||
case ".jpg":
|
||||
case ".jpeg": return "image/jpeg";
|
||||
case ".gif": return "image/gif";
|
||||
case ".bmp": return "image/bmp";
|
||||
case ".tif":
|
||||
case ".tiff": return "image/tiff";
|
||||
case ".webp": return "image/webp";
|
||||
case ".svg": return "image/svg+xml";
|
||||
|
||||
// Archives
|
||||
case ".zip": return "application/zip";
|
||||
case ".rar": return "application/x-rar-compressed";
|
||||
case ".7z": return "application/x-7z-compressed";
|
||||
case ".gz": return "application/gzip";
|
||||
case ".tar": return "application/x-tar";
|
||||
|
||||
// Email
|
||||
case ".eml": return "message/rfc822";
|
||||
case ".msg": return "application/vnd.ms-outlook";
|
||||
|
||||
// Audio / video — rare in legal mail but cheap to handle
|
||||
case ".mp3": return "audio/mpeg";
|
||||
case ".wav": return "audio/wav";
|
||||
case ".mp4": return "video/mp4";
|
||||
|
||||
default: return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
private static T? SafeGet<T>(Func<T> getter) where T : class
|
||||
{
|
||||
try { return getter(); } catch { return null; }
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace OutlookAddin.Services
|
||||
|
||||
vm.FileRequested += async (s, e) =>
|
||||
{
|
||||
try { await _host.LaunchFileToCaseAsync().ConfigureAwait(true); }
|
||||
try { await _host.LaunchFileToCaseAsync(e.PreselectedMatch).ConfigureAwait(true); }
|
||||
catch (Exception ex) { _logger.Warning(ex, "Sidebar file-request handler failed"); }
|
||||
};
|
||||
vm.OpenInBrowserRequested += (s, url) => TryOpenInBrowser(url);
|
||||
|
||||
Reference in New Issue
Block a user