Compare commits

...

4 Commits

Author SHA1 Message Date
59f26c3d3d Kleinere Robustheitsveränderungen
logging im program.cs gefixt - hier wurde der fälschlicherweise implementierte singleton statt der Wrapper-Klasse aufgerufen.

Retry eingebaut: Nach anlegen eines Kontakts wird jetzt 4x Geprüft ob der Kontakt angelegt wurde. Wenn nicht wird 1x das Anlegen wiederholt.

Methode checkt jetzt zwar den LastRun timestamp, hat aber eine Grateful period von 4h, in der sie die Kontakte nochmals prüft.
2025-10-11 20:41:20 +02:00
1f4ac29030 Stage 2025-02-12 10:18:48 +01:00
805e3c3b55 Initial Commit 2025-02-12 10:18:15 +01:00
37652d7ec2 .gitattributes und .gitignore hinzufügen. 2025-02-12 10:16:46 +01:00
14 changed files with 1856 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

364
.gitignore vendored Normal file
View File

@@ -0,0 +1,364 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/Epi2O365/appsettings.json

25
Epi2O365.sln Normal file
View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Epi2O365", "Epi2O365\Epi2O365.csproj", "{4D90F254-A69D-41CB-853D-77DE7DE56770}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4D90F254-A69D-41CB-853D-77DE7DE56770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D90F254-A69D-41CB-853D-77DE7DE56770}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D90F254-A69D-41CB-853D-77DE7DE56770}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D90F254-A69D-41CB-853D-77DE7DE56770}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E759632A-6321-4221-8A39-15C4E3DE0583}
EndGlobalSection
EndGlobal

91
Epi2O365/ConfigLoader.cs Normal file
View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System;
using System.IO;
using System.Text.Json;
namespace Epi2O365
{
public class ConfigLoader
{
public AzureAdConfig AzureAd { get; set; }
public GraphConfig Graph { get; set; }
public EpirentConfig Epirent { get; set; }
private static readonly string configFilePath = "appsettings.json";
private static ConfigLoader _instance;
// Singleton-Instanz für globale Nutzung
public static ConfigLoader Instance => _instance ??= LoadConfig();
public ConfigLoader() { }
/// <summary>
/// Lädt die Konfiguration aus der appsettings.json Datei
/// </summary>
public static ConfigLoader LoadConfig()
{
Logger.Info("Lade Konfigurationsdatei...");
try
{
if (!File.Exists(configFilePath))
{
Logger.Error($"Konfigurationsdatei '{configFilePath}' wurde nicht gefunden!");
throw new FileNotFoundException($"Config-Datei '{configFilePath}' wurde nicht gefunden!");
}
string json = File.ReadAllText(configFilePath);
var config = JsonSerializer.Deserialize<ConfigLoader>(json);
if (config == null)
{
Logger.Error("Fehler: Konfiguration konnte nicht deserialisiert werden.");
throw new Exception("Konfiguration konnte nicht geladen werden.");
}
Logger.Info("Konfiguration erfolgreich geladen.");
return config;
}
catch (Exception ex)
{
Logger.Error("Fehler beim Laden der Konfiguration!", ex);
throw;
}
}
}
/// <summary>
/// Azure AD Konfiguration (für Microsoft Graph API)
/// </summary>
public class AzureAdConfig
{
public string ClientId { get; set; }
public string TenantId { get; set; }
public string ClientSecret { get; set; }
}
/// <summary>
/// Microsoft Graph API Konfiguration
/// </summary>
public class GraphConfig
{
public string BaseUrl { get; set; }
public List<string> Users { get; set; }
}
/// <summary>
/// Epirent Konfiguration
/// </summary>
public class EpirentConfig
{
public string Server { get; set; }
public int Port { get; set; }
public string Token { get; set; }
public int Mandant { get; set; }
}
}

34
Epi2O365/Epi2O365.csproj Normal file
View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>Leopold Strobl</Authors>
<Company>VT-Media</Company>
<Title>Epirent to O365 Syncher</Title>
<Copyright>2025 VT-Media</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="log4net" Version="3.0.3" />
<PackageReference Include="Microsoft.Graph" Version="5.69.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.68.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RestSharp" Version="112.1.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.examples.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="log4net.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

89
Epi2O365/EpiContacts.cs Normal file
View File

@@ -0,0 +1,89 @@
using Microsoft.Graph.Models.ExternalConnectors;
using Microsoft.Kiota.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using RestSharp;
using Newtonsoft.Json;
namespace Epi2O365
{
class EpiContacts
{
ConfigLoader Configuration = ConfigLoader.Instance;
RestClient epirentserver;
public EpiContacts()
{
epirentserver = new RestClient("http://" + Configuration.Epirent.Server + ":" + Configuration.Epirent.Port);
}
private String getContactJson()
{
RestRequest request = new RestRequest("/v1/contact/filter?ia=1&cl=" + Configuration.Epirent.Mandant, RestSharp.Method.Get);
request.AddHeader("X-EPI-NO-SESSION", "True");
request.AddHeader("X-EPI-ACC-TOK", Configuration.Epirent.Token);
request.RequestFormat = DataFormat.Json;
return epirentserver.ExecuteGet(request).Content;
}
private String getContactDetailJson(long ContactPrimaryKey)
{
RestRequest request = new RestRequest("/v1/contact/"+ContactPrimaryKey+"/filter?ia=1&cl=" + Configuration.Epirent.Mandant, RestSharp.Method.Get);
request.AddHeader("X-EPI-NO-SESSION", "True");
request.AddHeader("X-EPI-ACC-TOK", Configuration.Epirent.Token);
request.RequestFormat = DataFormat.Json;
return epirentserver.ExecuteGet(request).Content;
}
private String getContactPersonJson(long ContactPersonPrimaryKey)
{
RestRequest request = new RestRequest("/v1/cperson/" + ContactPersonPrimaryKey + "/filter?ia=1&cl=" + Configuration.Epirent.Mandant, RestSharp.Method.Get);
request.AddHeader("X-EPI-NO-SESSION", "True");
request.AddHeader("X-EPI-ACC-TOK", Configuration.Epirent.Token);
request.RequestFormat = DataFormat.Json;
return epirentserver.ExecuteGet(request).Content;
}
public Model.ContactList getContactList()
{
Model.ContactList contactList = JsonConvert.DeserializeObject<Model.ContactList>(getContactJson());
return contactList;
}
public Model.ContactList getContactList(DateTime FilterTime)
{
Model.ContactList contactList = JsonConvert.DeserializeObject<Model.ContactList>(getContactJson());
contactList.Payload.RemoveAll(p => p.getDateTimeChanged() < FilterTime);
return contactList;
}
public Model.ContactDetail GetContactDetail(long primaryKey)
{
Model.ContactDetail contactDetail = JsonConvert.DeserializeObject<Model.ContactDetail>(getContactDetailJson(primaryKey));
return contactDetail;
}
public Model.ContactPersonDetail GetContactPersonDetail(long primaryKey)
{
Model.ContactPersonDetail cPersonDetail = JsonConvert.DeserializeObject<Model.ContactPersonDetail>(getContactPersonJson(primaryKey));
return cPersonDetail;
}
}
}

27
Epi2O365/Logger.cs Normal file
View File

@@ -0,0 +1,27 @@
using log4net.Config;
using log4net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Epi2O365
{
public static class Logger
{
private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
static Logger()
{
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));
}
public static void Info(string message) => log.Info(message);
public static void Warn(string message) => log.Warn(message);
public static void Error(string message, Exception ex = null) => log.Error(message, ex);
public static void Debug(string message) => log.Debug(message);
}
}

View File

@@ -0,0 +1,303 @@
using Microsoft.Graph.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Epi2O365.Model
{
public partial class ContactDetail
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("namespace")]
public string Namespace { get; set; }
[JsonProperty("action")]
public string Action { get; set; }
[JsonProperty("templateSignature")]
public string TemplateSignature { get; set; }
[JsonProperty("payload_length")]
public long PayloadLength { get; set; }
[JsonProperty("is_send_payload")]
public bool IsSendPayload { get; set; }
[JsonProperty("payload")]
public List<ContactDetailPayload> Payload { get; set; }
}
public partial class ContactDetailPayload
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("customer_no")]
public long CustomerNo { get; set; }
[JsonProperty("supplier_no")]
public long SupplierNo { get; set; }
[JsonProperty("is_single_person")]
public bool IsSinglePerson { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("name1")]
public string Name1 { get; set; }
[JsonProperty("name2")]
public string Name2 { get; set; }
[JsonProperty("name3")]
public string Name3 { get; set; }
[JsonProperty("matchcode")]
public string Matchcode { get; set; }
[JsonProperty("is_interested")]
public bool IsInterested { get; set; }
[JsonProperty("is_customer")]
public bool IsCustomer { get; set; }
[JsonProperty("is_supplier")]
public bool IsSupplier { get; set; }
[JsonProperty("is_location")]
public bool IsLocation { get; set; }
[JsonProperty("is_vip")]
public bool IsVip { get; set; }
[JsonProperty("is_colleague")]
public bool IsColleague { get; set; }
[JsonProperty("salutation")]
public string Salutation { get; set; }
[JsonProperty("grade")]
public string Grade { get; set; }
[JsonProperty("notes_positive")]
public string NotesPositive { get; set; }
[JsonProperty("notes_negative")]
public string NotesNegative { get; set; }
[JsonProperty("date_of_birth")]
public string DateOfBirth { get; set; }
[JsonProperty("date_changed")]
public DateTime DateChanged { get; set; }
[JsonProperty("time_changed")]
public long TimeChanged { get; set; }
[JsonProperty("date_created")]
public DateTime DateCreated { get; set; }
[JsonProperty("time_created")]
public long TimeCreated { get; set; }
public DateTime getDateTimeCreated()
{
TimeSpan time = TimeSpan.FromSeconds(TimeCreated);
return DateCreated.Date + time; // Combine date and time
}
public DateTime getDateTimeChanged()
{
TimeSpan time = TimeSpan.FromSeconds(TimeChanged);
return DateChanged.Date + time; // Combine date and time
}
[JsonProperty("is_active")]
public bool IsActive { get; set; }
[JsonProperty("terms_of_payment_pk")]
public long TermsOfPaymentPk { get; set; }
[JsonProperty("is_sysstd_terms_of_payment")]
public bool IsSysstdTermsOfPayment { get; set; }
[JsonProperty("is_blocked_order")]
public bool IsBlockedOrder { get; set; }
[JsonProperty("is_separate_invoice_address")]
public bool IsSeparateInvoiceAddress { get; set; }
[JsonProperty("has_image")]
public bool HasImage { get; set; }
[JsonProperty("address")]
public Address Address { get; set; }
[JsonProperty("address_list")]
public List<Address> AddressList { get; set; }
[JsonProperty("address_delivery")]
public object AddressDelivery { get; set; }
[JsonProperty("communication")]
public List<Communication> Communication { get; set; }
[JsonProperty("_communication_length")]
public long CommunicationLength { get; set; }
[JsonProperty("contact_person")]
public List<ContactPerson> ContactPerson { get; set; }
[JsonProperty("_contact_person_length")]
public long ContactPersonLength { get; set; }
[JsonProperty("_contact_chrono_length")]
public long ContactChronoLength { get; set; }
[JsonProperty("_order_length")]
public long OrderLength { get; set; }
[JsonProperty("searchindex")]
public List<object> Searchindex { get; set; }
[JsonProperty("_searchindex_length")]
public long SearchindexLength { get; set; }
[JsonProperty("contact_data")]
public ContactData ContactData { get; set; }
[JsonProperty("_address_list_length")]
public long AddressListLength { get; set; }
[JsonProperty("_contact_data_length")]
public long ContactDataLength { get; set; }
}
public partial class Address
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("postal_code")]
public string PostalCode { get; set; }
[JsonProperty("street")]
public string Street { get; set; }
[JsonProperty("city")]
public string City { get; set; }
[JsonProperty("country")]
public string Country { get; set; }
[JsonProperty("country_iso3")]
public string CountryIso3 { get; set; }
[JsonProperty("federal_state")]
public string FederalState { get; set; }
}
public partial class Communication
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("contact_pk")]
public long ContactPk { get; set; }
[JsonProperty("contact_person_pk")]
public long ContactPersonPk { get; set; }
[JsonProperty("uplink")]
public string Uplink { get; set; }
[JsonProperty("type")]
public long Type { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("phone_match_number")]
public string PhoneMatchNumber { get; set; }
[JsonProperty("position")]
public long Position { get; set; }
[JsonProperty("is_newsletter")]
public bool IsNewsletter { get; set; }
[JsonProperty("is_invoice")]
public bool IsInvoice { get; set; }
}
public partial class ContactPerson
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("contact_pk")]
public long ContactPk { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("first_name")]
public string FirstName { get; set; }
[JsonProperty("last_name")]
public string LastName { get; set; }
[JsonProperty("nickname")]
public string Nickname { get; set; }
[JsonProperty("position")]
public string Position { get; set; }
[JsonProperty("has_image")]
public bool HasImage { get; set; }
[JsonProperty("_ref_contact")]
public string RefContact { get; set; }
[JsonProperty("_ref_contact_person")]
public string RefContactPerson { get; set; }
[JsonProperty("date_changed")]
public DateTimeOffset DateChanged { get; set; }
[JsonProperty("time_changed")]
public long TimeChanged { get; set; }
[JsonProperty("user_changed")]
public string UserChanged { get; set; }
[JsonProperty("date_created")]
public DateTimeOffset DateCreated { get; set; }
[JsonProperty("time_created")]
public long TimeCreated { get; set; }
[JsonProperty("user_created")]
public string UserCreated { get; set; }
}
}

View File

@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Graph.Models;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Epi2O365.Model
{
public partial class ContactList
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("namespace")]
public string Namespace { get; set; }
[JsonProperty("action")]
public string Action { get; set; }
[JsonProperty("templateSignature")]
public string TemplateSignature { get; set; }
[JsonProperty("payload_length")]
public long PayloadLength { get; set; }
[JsonProperty("is_send_payload")]
public bool IsSendPayload { get; set; }
[JsonProperty("payload")]
public List<Payload> Payload { get; set; }
//[JsonProperty("filter_options")]
//public FilterOptions FilterOptions { get; set; }
//[JsonProperty("request_duration")]
//public long RequestDuration { get; set; }
//[JsonProperty("req_datetime")]
//public ReqDatetime ReqDatetime { get; set; }
//[JsonProperty("_dbg_request_data")]
//public DbgRequestData DbgRequestData { get; set; }
//[JsonProperty("_dbg_user_info")]
//public UserInfo DbgUserInfo { get; set; }
}
public partial class Payload
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("customer_no")]
public long CustomerNo { get; set; }
[JsonProperty("supplier_no")]
public long SupplierNo { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("name1")]
public string Name1 { get; set; }
[JsonProperty("name2")]
public string Name2 { get; set; }
[JsonProperty("name3")]
public string Name3 { get; set; }
[JsonProperty("matchcode")]
public string Matchcode { get; set; }
[JsonProperty("salutation")]
public string Salutation { get; set; }
[JsonProperty("grade")]
public string Grade { get; set; }
[JsonProperty("is_interested")]
public bool IsInterested { get; set; }
[JsonProperty("is_customer")]
public bool IsCustomer { get; set; }
[JsonProperty("is_supplier")]
public bool IsSupplier { get; set; }
[JsonProperty("is_location")]
public bool IsLocation { get; set; }
[JsonProperty("is_vip")]
public bool IsVip { get; set; }
[JsonProperty("is_colleague")]
public bool IsColleague { get; set; }
[JsonProperty("date_changed")]
public DateTime DateChanged { get; set; }
[JsonProperty("time_changed")]
public long TimeChanged { get; set; }
[JsonProperty("date_created")]
public DateTime DateCreated { get; set; }
[JsonProperty("time_created")]
public long TimeCreated { get; set; }
public DateTime getDateTimeCreated()
{
TimeSpan time = TimeSpan.FromSeconds(TimeCreated);
return DateCreated.Date + time; // Combine date and time
}
public DateTime getDateTimeChanged()
{
TimeSpan time = TimeSpan.FromSeconds(TimeChanged);
return DateChanged.Date + time; // Combine date and time
}
[JsonProperty("is_active")]
public bool IsActive { get; set; }
[JsonProperty("terms_of_payment_pk")]
public long TermsOfPaymentPk { get; set; }
[JsonProperty("is_sysstd_terms_of_payment")]
public bool IsSysstdTermsOfPayment { get; set; }
[JsonProperty("has_image")]
public bool HasImage { get; set; }
[JsonProperty("_ref_contact")]
public string RefContact { get; set; }
[JsonProperty("contact_data")]
public ContactData ContactData { get; set; }
[JsonProperty("_contact_data_length", NullValueHandling = NullValueHandling.Ignore)]
public long? ContactDataLength { get; set; }
}
public partial class ContactData
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("contact_pk")]
public long ContactPk { get; set; }
[JsonProperty("supplier_no")]
public long SupplierNo { get; set; }
[JsonProperty("customer_no")]
public long CustomerNo { get; set; }
[JsonProperty("debitor_no")]
public string DebitorNo { get; set; }
[JsonProperty("creditor_no")]
public string CreditorNo { get; set; }
[JsonProperty("rentshop_contact_uri")]
public string RentshopContactUri { get; set; }
[JsonProperty("is_rentshop_active")]
public bool IsRentshopActive { get; set; }
[JsonProperty("discount_rent_customer")]
public long DiscountRentCustomer { get; set; }
[JsonProperty("discount_sale_customer")]
public long DiscountSaleCustomer { get; set; }
[JsonProperty("is_vat_active_customer")]
public bool IsVatActiveCustomer { get; set; }
[JsonProperty("country_type_customer")]
public long CountryTypeCustomer { get; set; }
[JsonProperty("discount_rent_supplier")]
public long DiscountRentSupplier { get; set; }
[JsonProperty("discount_sale_supplier")]
public double DiscountSaleSupplier { get; set; }
[JsonProperty("is_vat_active_supplier")]
public bool IsVatActiveSupplier { get; set; }
[JsonProperty("country_type_supplier")]
public long CountryTypeSupplier { get; set; }
}
}

View File

@@ -0,0 +1,154 @@
using Microsoft.Graph.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Epi2O365.Model
{
public partial class ContactPersonDetail
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("namespace")]
public string Namespace { get; set; }
[JsonProperty("action")]
public string Action { get; set; }
[JsonProperty("templateSignature")]
public string TemplateSignature { get; set; }
[JsonProperty("payload_length")]
public long PayloadLength { get; set; }
[JsonProperty("is_send_payload")]
public bool IsSendPayload { get; set; }
[JsonProperty("payload")]
public List<ContactPersonPayload> Payload { get; set; }
}
public partial class ContactPersonPayload
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("contact_pk")]
public long ContactPk { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("first_name")]
public string FirstName { get; set; }
[JsonProperty("last_name")]
public string LastName { get; set; }
[JsonProperty("nickname")]
public string Nickname { get; set; }
[JsonProperty("position")]
public string Position { get; set; }
[JsonProperty("date_of_birth")]
public string DateOfBirth { get; set; }
[JsonProperty("department")]
public string Department { get; set; }
[JsonProperty("salutation")]
public string Salutation { get; set; }
[JsonProperty("grade")]
public string Grade { get; set; }
[JsonProperty("has_image")]
public bool HasImage { get; set; }
[JsonProperty("communication")]
public List<ContactPersonCommunication> Communication { get; set; }
[JsonProperty("searchindex")]
public List<object> Searchindex { get; set; }
[JsonProperty("date_changed")]
public DateTimeOffset DateChanged { get; set; }
[JsonProperty("time_changed")]
public long TimeChanged { get; set; }
[JsonProperty("user_changed")]
public string UserChanged { get; set; }
[JsonProperty("date_created")]
public DateTimeOffset DateCreated { get; set; }
[JsonProperty("time_created")]
public long TimeCreated { get; set; }
[JsonProperty("user_created")]
public string UserCreated { get; set; }
public DateTime getDateTimeCreated()
{
TimeSpan time = TimeSpan.FromSeconds(TimeCreated);
return DateCreated.Date + time; // Combine date and time
}
public DateTime getDateTimeChanged()
{
TimeSpan time = TimeSpan.FromSeconds(TimeChanged);
return DateChanged.Date + time; // Combine date and time
}
[JsonProperty("_communication_length")]
public long CommunicationLength { get; set; }
[JsonProperty("_contact_chrono_length")]
public long ContactChronoLength { get; set; }
}
public partial class ContactPersonCommunication
{
[JsonProperty("primary_key")]
public long PrimaryKey { get; set; }
[JsonProperty("contact_pk")]
public long ContactPk { get; set; }
[JsonProperty("contact_person_pk")]
public long ContactPersonPk { get; set; }
[JsonProperty("uplink")]
public string Uplink { get; set; }
[JsonProperty("type")]
public long Type { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("phone_match_number")]
public string PhoneMatchNumber { get; set; }
[JsonProperty("position")]
public long Position { get; set; }
[JsonProperty("is_newsletter")]
public bool IsNewsletter { get; set; }
[JsonProperty("is_invoice")]
public bool IsInvoice { get; set; }
}
}

258
Epi2O365/O365Connector.cs Normal file
View File

@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions.Authentication;
using System.ComponentModel.Design;
using log4net;
using System.Threading;
namespace Epi2O365
{
internal class O365Connector
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(O365Connector));
private string clientID;
private string clientSecret;
private string tenantID;
private string BaseUrl;
private const int CreateVerifyDelayMs = 2500;
private const int CreateVerifyMaxRetries = 4;
private const int CreateHttpRetryMax = 1; // 1 zusätzlicher Versuch bei 429/5xx
private const int CreateRetryCount = 1; // Anzahl zusätzlicher Versuche
public O365Connector(string clientID, string clientSecret, string tenantID, string BaseUrl)
{
this.clientID = clientID;
this.clientSecret = clientSecret;
this.tenantID = tenantID;
this.BaseUrl = BaseUrl;
}
// OData-Escape für Filterwerte
private static string O(string s) => (s ?? string.Empty).Replace("'", "''");
public async Task SyncContact(Contact contact, string destinationAccountPrimaryAddress, int depth = 0)
{
try
{
var graphClient = await GetGraphClient();
if (await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient))
{
Logger.Info(destinationAccountPrimaryAddress + " | Contact " + contact.DisplayName + " found in O365");
string O365ContactID = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName);
if (!string.IsNullOrEmpty(O365ContactID))
{
await graphClient.Users[destinationAccountPrimaryAddress]
.Contacts[O365ContactID]
.PatchAsync(contact);
Logger.Info(destinationAccountPrimaryAddress + $" | Contact with ID '{O365ContactID}' successfully updated");
}
else
{
Logger.Warn(destinationAccountPrimaryAddress + " | No valid contact ID found for update.");
}
}
else
{
// --- Create-Flow mit NickName-basierter Verifikation ---
Logger.Info(destinationAccountPrimaryAddress + " | Contact " + contact.DisplayName + " NOT found in O365");
Logger.Info(destinationAccountPrimaryAddress + " | Creating new Contact (Nick='" + contact.NickName + "')");
// kleiner HTTP-Retry nur für den POST (429/5xx)
int httpAttempts = 0;
while (true)
{
try
{
// Prefer: return=representation liefert dir direkt eine ID (nur fürs Log)
var created = await graphClient.Users[destinationAccountPrimaryAddress]
.Contacts
.PostAsync(contact, rc => rc.Headers.Add("Prefer", "return=representation"));
Logger.Info(destinationAccountPrimaryAddress + " | Created contact: Id=" + (created?.Id ?? "n/a") + " Nick='" + (created?.NickName ?? contact.NickName) + "'");
// NickName sicherstellen (einmaliges PATCH idempotent)
if (!string.IsNullOrEmpty(created?.Id) && !string.IsNullOrWhiteSpace(contact.NickName))
{
try
{
await graphClient.Users[destinationAccountPrimaryAddress]
.Contacts[created.Id]
.PatchAsync(new Contact { NickName = contact.NickName });
Logger.Debug(destinationAccountPrimaryAddress + $" | Ensured nickName='{contact.NickName}' on Id={created.Id}");
}
catch (Exception ex)
{
Logger.Warn(destinationAccountPrimaryAddress + $" | Unable to ensure nickName on Id={created.Id}: {ex.Message}");
}
}
break; // POST erfolgreich
}
catch (Microsoft.Kiota.Abstractions.ApiException apiEx) when (
(apiEx.ResponseStatusCode >= 500 || apiEx.ResponseStatusCode == 429)
&& httpAttempts < CreateHttpRetryMax)
{
httpAttempts++;
Logger.Warn(destinationAccountPrimaryAddress + $" | Create POST failed with {apiEx.ResponseStatusCode}. Retry {httpAttempts}/{CreateHttpRetryMax} in 1s …");
await Task.Delay(1000);
}
}
// Verifizieren mit bis zu N Wiederholungen immer dieselbe NickName-Checkmethode
for (int attempt = 0; attempt <= CreateVerifyMaxRetries; attempt++)
{
await Task.Delay(CreateVerifyDelayMs);
Logger.Info(destinationAccountPrimaryAddress + $" | Starting to Verify: contact '{contact.NickName}'. (attempt {attempt}).");
bool existsNow = await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient);
if (existsNow)
{
Logger.Info(destinationAccountPrimaryAddress + $" | Verified: contact '{contact.NickName}' is present after create (attempt {attempt}).");
// Optional direkt noch die ID loggen:
var id = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName);
if (!string.IsNullOrEmpty(id))
Logger.Debug(destinationAccountPrimaryAddress + $" | Verified ContactId: {id}");
return;
}
if (attempt == CreateVerifyMaxRetries)
{
Logger.Warn(destinationAccountPrimaryAddress + $" | Contact '{contact.NickName}' still not visible after create (attempts: {CreateVerifyMaxRetries + 1}).");
}
else
{
Logger.Info(destinationAccountPrimaryAddress + $" | Not visible yet, re-checking (attempt {attempt + 1}/{CreateVerifyMaxRetries}) …");
}
}
// optional: eine vorsichtige Wiederholung mit gleicher Logik (NickName-only)
if (depth < CreateRetryCount)
{
Logger.Warn(destinationAccountPrimaryAddress + $" | Retrying SyncContact once for Nick='{contact.NickName}' (depth {depth + 1}) …");
await Task.Delay(2000);
await SyncContact(contact, destinationAccountPrimaryAddress, depth + 1);
}
}
}
catch (Exception ex)
{
Logger.Error(destinationAccountPrimaryAddress + " | Error while synchronizing contact: " + ex.Message);
}
}
private async Task<string> GetContactIdByNickName(GraphServiceClient graphClient, string userPrimaryMail, string nickName)
{
try
{
var safe = O(nickName);
var contacts = await graphClient.Users[userPrimaryMail]
.Contacts
.GetAsync(rc =>
{
rc.QueryParameters.Filter = $"nickName eq '{safe}'";
rc.QueryParameters.Select = new[] { "id", "nickName", "displayName" };
rc.QueryParameters.Top = 1;
});
var contact = contacts?.Value?.FirstOrDefault();
return contact?.Id;
}
catch (Exception ex)
{
Logger.Error(userPrimaryMail + $" | Error retrieving contact with NickName '{nickName}': {ex.Message}");
return null;
}
}
private async Task<bool> DoesContactExist(Contact contact, string userPrimaryMail, GraphServiceClient graphClient)
{
string NickName = contact.NickName;
if (string.IsNullOrWhiteSpace(NickName))
{
Logger.Warn(userPrimaryMail + " | No valid NickName found for the given contact.");
return false;
}
try
{
var safe = O(NickName);
var contacts = await graphClient.Users[userPrimaryMail]
.Contacts
.GetAsync(rc =>
{
rc.QueryParameters.Filter = $"nickName eq '{safe}'";
rc.QueryParameters.Select = new[] { "id" };
rc.QueryParameters.Top = 1;
});
bool exists = contacts?.Value != null && contacts.Value.Any();
Logger.Info(userPrimaryMail + $" | Contact with NickName {NickName} exists: {exists}");
return exists;
}
catch (Exception ex)
{
Logger.Error(userPrimaryMail + $" | Error checking contacts: {ex.Message}");
return false;
}
}
private async Task<GraphServiceClient> GetGraphClient()
{
try
{
var clientApp = ConfidentialClientApplicationBuilder.Create(clientID)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri($"{BaseUrl}/{tenantID}"))
.Build();
var accessTokenProvider = new TokenAcquisitionProvider(clientApp);
var authProvider = new BaseBearerTokenAuthenticationProvider(accessTokenProvider);
return new GraphServiceClient(authProvider);
}
catch (Exception ex)
{
Logger.Error("Error initializing GraphServiceClient: " + ex.Message);
throw;
}
}
}
public class TokenAcquisitionProvider : IAccessTokenProvider
{
private readonly IConfidentialClientApplication _clientApp;
public TokenAcquisitionProvider(IConfidentialClientApplication clientApp)
{
_clientApp = clientApp;
}
public async Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default, CancellationToken cancellationToken = default)
{
try
{
var result = await _clientApp.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync(cancellationToken);
return result.AccessToken;
}
catch (Exception ex)
{
Logger.Error("Error acquiring token: " + ex.Message);
throw;
}
}
public AllowedHostsValidator AllowedHostsValidator { get; } = new AllowedHostsValidator();
}
}

208
Epi2O365/Program.cs Normal file
View File

@@ -0,0 +1,208 @@
using System;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Configuration;
using Microsoft.Graph.Models;
using log4net;
using System.Net.Mail;
using System.IO;
using System.Linq;
namespace Epi2O365
{
class Program
{
//private static readonly ILog Logger = LogManager.GetLogger(typeof(Program));
private static ConfigLoader Configuration;
private static Model.ContactList ContactList;
private static O365Connector O365;
static async Task Main()
{
string pathLastRuntime = "LastRun.txt";
DateTime LastRun = DateTime.Parse("01.01.1970 10:00:00");
Logger.Info("Started2");
try
{
if (File.Exists(pathLastRuntime))
{
Logger.Debug("Last run file found. Attempting to parse datetime.");
string datetimeFromText = File.ReadAllText(pathLastRuntime);
Logger.Debug("Found datetime string: " + datetimeFromText);
LastRun = DateTime.Parse(datetimeFromText).AddHours(-4);
Logger.Info("Last run timestamp: " + LastRun.ToString());
}
}
catch (Exception ex)
{
Logger.Error("Failed to parse last run timestamp from " + pathLastRuntime + ". Error: " + ex.Message);
}
Logger.Info("Updating last run timestamp.");
updateLastRunTimestamp();
try
{
Logger.Info("Initializing configuration.");
Configuration = ConfigLoader.Instance;
Logger.Info("Configuration loaded successfully.");
}
catch (Exception ex)
{
Logger.Error("Error loading configuration: " + ex.Message);
return;
}
try
{
Logger.Info("Initializing O365 connector.");
O365 = new O365Connector(Configuration.AzureAd.ClientId, Configuration.AzureAd.ClientSecret, Configuration.AzureAd.TenantId, Configuration.Graph.BaseUrl);
Logger.Info("O365 connector initialized.");
}
catch (Exception ex)
{
Logger.Error("Error initializing O365 connector: " + ex.Message);
return;
}
try
{
Logger.Info("Fetching Epirent contacts.");
ContactList = new EpiContacts().getContactList(LastRun);
}
catch (Exception ex)
{
Logger.Error("Error fetching contacts: " + ex.Message);
return;
}
foreach (Model.Payload Contact in ContactList.Payload)
{
try
{
Logger.Debug("Processing contact: " + Contact.Name + " (Primary Key: " + Contact.PrimaryKey + ")");
Model.ContactDetail contactDetail = new EpiContacts().GetContactDetail(Contact.PrimaryKey);
var CompanyPhoneNumbers = contactDetail.Payload[0].Communication
.Where(c => c.Type == 0)
.Select(c => c.PhoneMatchNumber)
.ToList();
var CompanyMailAddresses = contactDetail.Payload[0].Communication
.Where(c => c.Type == 3)
.Select(c => new Microsoft.Graph.Models.EmailAddress
{
Address = c.Uplink,
Name = Contact.Name
})
.ToList();
var CompanyMobilePhoneNumber = contactDetail.Payload[0].Communication
.FirstOrDefault(c => c.Type == 2)?.PhoneMatchNumber;
Contact o365CompanyContact = new Contact
{
DisplayName = contactDetail.Payload[0].Name,
GivenName = contactDetail.Payload[0].Name,
Surname= contactDetail.Payload[0].Name1,
CompanyName = contactDetail.Payload[0].Name,
EmailAddresses = CompanyMailAddresses,
BusinessPhones = CompanyPhoneNumbers,
MobilePhone = CompanyMobilePhoneNumber,
NickName = Contact.PrimaryKey.ToString(),
};
foreach (string addressBookHolder in Configuration.Graph.Users)
{
try
{
O365.SyncContact(o365CompanyContact, addressBookHolder);
Logger.Info("Contact synchronized for user: " + addressBookHolder);
}
catch (Exception ex)
{
Logger.Error("Error synchronizing contact for " + addressBookHolder + ": " + ex.Message);
}
}
foreach (Model.ContactPerson contactPerson in contactDetail.Payload[0].ContactPerson)
{
try
{
Logger.Debug("Processing contact person: " + contactPerson.Name);
Model.ContactPersonPayload cperson = new EpiContacts().GetContactPersonDetail(contactPerson.PrimaryKey).Payload[0];
var phoneNumbers = cperson.Communication
.Where(c => c.Type == 0)
.Select(c => c.PhoneMatchNumber)
.ToList();
var mailAddresses = cperson.Communication
.Where(c => c.Type == 3)
.Select(c => new Microsoft.Graph.Models.EmailAddress
{
Address = c.Uplink,
Name = cperson.Name
})
.ToList();
var MobilePhoneNumber = cperson.Communication
.FirstOrDefault(c => c.Type == 2)?.PhoneMatchNumber;
Contact o365contact = new Contact
{
GivenName = cperson.FirstName,
Surname = cperson.LastName,
DisplayName = cperson.Name,
CompanyName = contactDetail.Payload[0].Name,
Department = cperson.Department,
JobTitle = cperson.Position,
EmailAddresses = mailAddresses,
BusinessPhones = phoneNumbers,
MobilePhone = MobilePhoneNumber,
NickName = Contact.PrimaryKey + "-" + cperson.PrimaryKey
};
foreach (string addressBookHolder in Configuration.Graph.Users)
{
try
{
await O365.SyncContact(o365contact, addressBookHolder);
Logger.Info("Contact person synchronized for user: " + addressBookHolder);
}
catch (Exception ex)
{
Logger.Error("Error synchronizing contact person for " + addressBookHolder + ": " + ex.Message);
}
}
}
catch (Exception ex)
{
Logger.Error("Error processing contact person " + contactPerson.Name + ": " + ex.Message);
}
}
}
catch (Exception ex)
{
Logger.Error("Error processing contact " + Contact.Name + ": " + ex.Message);
}
}
}
private static void updateLastRunTimestamp()
{
string fileName = "LastRun.txt";
try
{
File.WriteAllText(fileName, DateTime.Now.ToString());
Logger.Info("Last run timestamp updated successfully.");
}
catch (Exception ex)
{
Logger.Error("Error updating last run timestamp: " + ex.Message);
}
}
}
}

View File

@@ -0,0 +1,19 @@
{
"AzureAd": {
"ClientId": "xxx",
"TenantId": "xxx",
"ClientSecret": "xxx"
},
"Graph": {
"BaseUrl": "https://login.microsoftonline.com",
"Users": [
"xx@yy.de"
]
},
"Epirent": {
"Server": "xx",
"Port": 8080,
"Token": "xx",
"Mandant": 2
}
}

24
Epi2O365/log4net.config Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<log4net>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logs/app.log" />
<appendToFile value="true" />
<rollingStyle value="Date" />
<datePattern value="yyyy-MM-dd'.log'" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="[%date] [%level] %logger - %message%newline" />
</layout>
</appender>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="[%date] [%level] %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="RollingFileAppender" />
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>