04.09.2013 Views

integration mellem silverpop og pivotal - Danmarks Tekniske ...

integration mellem silverpop og pivotal - Danmarks Tekniske ...

integration mellem silverpop og pivotal - Danmarks Tekniske ...

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

Christian Schoubye Hansen<br />

Kongens Lyngby, januar 2010<br />

IMM-B.Eng-2010-01<br />

INTEGRATION<br />

MELLEM<br />

SILVERPOP OG PIVOTAL


Technical University of Denmark<br />

Informatics and Mathematical Modelling<br />

DTU – Building 321, DK-2800 Kongens Lyngby, Denmark<br />

Phone +45 45253351, Fax +45 45882673<br />

reception@imm.dtu.dk<br />

www.imm.dtu.dk<br />

IMM-B.Eng-2010-01<br />

Side 2


Abstrakt<br />

Hos FOSS er man interesseret i at kunne lave nye former marketing kampagner, som kan generere<br />

mere salg.<br />

Silverpop er et amerikansk firma, som har specialiseret sig i email marketing kampagner.<br />

Ved at bruge deres produkt, kan man lave dynamiske kampagner. Hvor man leverer lige præcist<br />

den information, som er inden for kundens forretningsområde.<br />

Målet med dette projekt er at lave synkronisering begge veje <strong>mellem</strong> Silverpop <strong>og</strong> FOSS’ CRM<br />

system, Pivotal.<br />

Endvidere skal data kunne behandles i Pivotal <strong>og</strong> kunder skal have mulighed for at kunne afmelde<br />

sig marketings e-mails fra FOSS.<br />

Side 3


Forord<br />

Denne rapport er en afhandling for en Bachelor grad i Engineering. Den blev lavet ved Institut for<br />

Informatik <strong>og</strong> Matematisk Modellering, ved <strong>Danmarks</strong> <strong>Tekniske</strong> Universitet.<br />

Afhandlingen omhandler synkronisering <strong>mellem</strong> 2 computer systemer. Den er inddelt i 3<br />

hovedområder, først analyse af de forskellige platforme, derefter designe en god måde at<br />

implementere en synkronisering <strong>og</strong> til sidst selve implementationen.<br />

Afhandlingen er lavet <strong>mellem</strong> 2/11-2009 <strong>og</strong> 22/01 2010<br />

Christian Schoubye Hansen<br />

Kongens Lyngby, januar 2010<br />

Side 4


Indholdsfortegnelse<br />

Abstrakt ................................................................................................................................................ 3<br />

Forord ................................................................................................................................................... 4<br />

Indledning ............................................................................................................................................ 7<br />

FOSS ................................................................................................................................................. 7<br />

Silverpop .......................................................................................................................................... 7<br />

Krav specifikation til <strong>integration</strong>sprojektet ..................................................................................... 8<br />

Analyse ................................................................................................................................................. 9<br />

Platformens Teknol<strong>og</strong>i ..................................................................................................................... 9<br />

Pivotal Tabel design / konstruktion ............................................................................................... 11<br />

Tabel vindue ............................................................................................................................... 11<br />

Pivotal Forms .................................................................................................................................. 13<br />

Pivotal agenter ............................................................................................................................... 14<br />

Løkker i agenter ......................................................................................................................... 14<br />

Silverpop ........................................................................................................................................ 15<br />

Data der skal sendes fra Pivotal til Silverpop............................................................................ 16<br />

Data fra Silverpop til Pivotal ...................................................................................................... 16<br />

Design ................................................................................................................................................. 17<br />

Tabel opbygning i Silverpop. .......................................................................................................... 17<br />

Nye tabeller i Pivotal ...................................................................................................................... 17<br />

Synkronisering af data fra Pivotal til Silverpop .............................................................................. 18<br />

Synkronisering fra Silverpop til Pivotal .......................................................................................... 19<br />

Unsubscribe .................................................................................................................................... 19<br />

Implementering.................................................................................................................................. 20<br />

Unsubscribe agent ......................................................................................................................... 20<br />

Data fra Pivotal til Silverpop agent ................................................................................................ 22<br />

Data fra Silverpop til Pivotal agent ................................................................................................ 24<br />

Data fra Pivotal til Silverpop C# kode ............................................................................................ 25<br />

ConsoleApplication1 / SilverpopIntegration .............................................................................. 25<br />

Data fra Silverpop til Pivotal C# kode ............................................................................................ 26<br />

Return Silverpop Data ................................................................................................................ 26<br />

Side 5


Test ..................................................................................................................................................... 28<br />

Konklusion .......................................................................................................................................... 29<br />

Opfyldte krav i forhold til løsningen .............................................................................................. 29<br />

Tilfredshed fra marketing afdelingen ............................................................................................ 29<br />

Benefit for forretningen ................................................................................................................. 29<br />

Fremtidig udvikling ......................................................................................................................... 29<br />

Bilag .................................................................................................................................................... 30<br />

Pivotal Agenter ............................................................................................................................... 30<br />

Return Silverpop Data C# ............................................................................................................... 32<br />

App.config .................................................................................................................................. 32<br />

ByteConvertor.cs ........................................................................................................................ 33<br />

CsvParser.cs ................................................................................................................................ 35<br />

ErrorMailGenerator.cs ............................................................................................................... 38<br />

L<strong>og</strong>.cs .......................................................................................................................................... 39<br />

SilverpopAPI.cs ........................................................................................................................... 41<br />

SilverpopFTP.cs .......................................................................................................................... 50<br />

TransactionalDatabase.cs .......................................................................................................... 52<br />

Pr<strong>og</strong>ram.cs ................................................................................................................................. 54<br />

ConsoleApplication1 C# ................................................................................................................. 57<br />

App.config ...................................................................................................................................... 57<br />

ByteConvertor.cs ........................................................................................................................ 58<br />

CsvFileGenerator.cs ................................................................................................................... 60<br />

SilverpopAPI.cs ........................................................................................................................... 64<br />

SilverpopFTP.cs .......................................................................................................................... 72<br />

Pr<strong>og</strong>ram.cs ................................................................................................................................. 74<br />

MainList.xml ............................................................................................................................... 80<br />

Side 6


Indledning<br />

Konkurrencen om kunderne <strong>og</strong> salg skærpes væsentligt med den nuværende krise. Hos FOSS<br />

sælger man et mindre antal apparater men til en meget høj pris per styk. Dermed er ethvert salg<br />

af allerstørste vigtighed for virksomheden. Man er derfor interesseret i nye tiltag, som kan<br />

generere mere salg.<br />

Et af de nye tiltag hos FOSS er ”Silverpop Engage”. Der er tale om en platform, der vil gøre det<br />

muligt at lave dynamiske marketing kampagner. Det er nødvendigt at gøre det nemt for<br />

marketingsafdelingen at bruge Silverpop, <strong>og</strong> derfor ønsker man at integrere Silverpop med FOSS’<br />

CRM system, som indeholder alle kundeoplysninger.<br />

FOSS<br />

FOSS blev grundlagt i 1956 af Nils Foss. Målet var at lave automatiske analyse apparater til<br />

landbruget. Tidligere havde man brugt manuelle analyse metoder, hvilket var meget<br />

tidskrævende.<br />

Man opfandt et apparat, som var lille <strong>og</strong> meget robust. Landmanden kunne tage det med ud på<br />

marken <strong>og</strong> hurtigt måle indholdet af fugt i korn. Det blev en stor succes.<br />

Senere begyndte man <strong>og</strong>så at lave analyse apparater til mejeri <strong>og</strong> kød industrien. Med købet<br />

af Perstorp Analytical AB i 1997, blev konceptet udvidet til <strong>og</strong>så at omfatte produktion af analyse<br />

apparater til mel, øl, farmaceutisk <strong>og</strong> kemisk industri.<br />

FOSS er i dag en global virksomhed med salgskontorer over det meste af verdenen. Det er stadig<br />

en familieejet virksomhed, der i dag er ledet af Nils Foss’ søn Peter Foss.<br />

Silverpop<br />

Silverpop er et amerikansk firma, der har udviklet en web baseret platform, som virksomheder kan<br />

benytte til at lave marketing kampagner.<br />

Silverpop giver mulighed for at vælge <strong>mellem</strong> forskellige informationsformer f.eks ved udsendelse<br />

af e-mail eller sms.<br />

Man har <strong>og</strong>så mulighed for at få en rapport over en marketing kampagne. I den kan man f.eks. se<br />

om en e-mail er bounced, dvs. den ikke nåede frem til modtageren. Derudover kan man se om<br />

modtageren åbnede e-mailen <strong>og</strong> om modtageren klikkede på n<strong>og</strong>le links i e-mailen.<br />

Man kan <strong>og</strong>så lave dynamiske e-mails, som indeholder generel information til alle kunder <strong>og</strong><br />

specifik information til alle kunder indenfor et marked segment. Når man sender<br />

nyhedsinformationen ud, baseres den på et eller flere kriterier. Eksempel på specifik information:<br />

kødindustrien får nyheder om kødprodukter <strong>og</strong> mejerier får nyheder om mejeriprodukter.<br />

Side 7


Krav specifikation til <strong>integration</strong>sprojektet<br />

Det er et krav at kunne synkronise data fra Pivotal til Silverpop. Det drejer sig om contacts, leads<br />

<strong>og</strong> prospects. Contacts er kunder, der har købt hos FOSS før. Leads <strong>og</strong> prospects er nye potentielle<br />

kunder, der har udtrykt interesse for FOSS produkter.<br />

Forskellen på et lead <strong>og</strong> et prospect er mængden af data om kunden. Hvis der kun er et navn <strong>og</strong> en<br />

e-mail, så bliver personen oprettet som et prospect. Når man får flere data, kan man så opgradere<br />

personen til et lead.<br />

Det er et krav at kunne synkronisere data fra Silverpop tilbage til Pivotal. Her er det data om de<br />

enkelte e-mails, man er interesseret i. Om en e-mail kom frem eller om den blev bounced. Om<br />

kunden åbnede e-mailen <strong>og</strong> evt. klikkede på n<strong>og</strong>le links i den.<br />

De forskellige skærmbilleder skal opdateres i Pivotal, så man kan se, hvordan den enkelte<br />

marketing kampagne gik. Hvor mange der modt<strong>og</strong> e-mailen, <strong>og</strong> hvor mange der var interesseret i<br />

indholdet.<br />

Der skal <strong>og</strong>så laves en unsubscribe funktion, så kunder kan afmelde nyheds- <strong>og</strong> reklame e-mails.<br />

Dette er et lov krav. Det skal både kunne lade sig gøre i Pivotal <strong>og</strong> via en hjemmeside.<br />

Det skal <strong>og</strong>så være muligt at resubscribe igen, hvis en kunde ønsker det.<br />

Unsubscribe via hjemmesiden er ikke en del af dette projekt. Det vil de udviklere, der<br />

pr<strong>og</strong>rammerer FOSS’ hjemmeside sørge for kommer til at virke.<br />

Når en kunde bliver afmeldt nyheds e-mails, skal de interesseområder, som kunden i første<br />

omgang skrev sig op til, slettes, så de ikke indgår i søgeresultater fra queries.<br />

Det sidste krav er at designet af databasen <strong>og</strong> synkroniseringspr<strong>og</strong>rammet skal gøre yderligere<br />

udvikling mulig. Dette er nødvendig for både for at kunne imødekomme nye ønsker fra marketing<br />

afdelingen, <strong>og</strong> <strong>og</strong>så for at kunne tilpasse synkroniseringspr<strong>og</strong>rammet til et nyt CRM system.<br />

Side 8


Analyse<br />

Platformens Teknol<strong>og</strong>i<br />

Hos FOSS bruger man Pivotal eRelationship CRM system.<br />

CRM står for “Customer Relationship Management” system.<br />

Systemet bruges til 3 hovedområder.<br />

Marketing til at holde styr på potentielle kunder samt kunder, som har handlet før hos FOSS. Det<br />

vil sige anvendelse af disse oplysninger i forbindelse med marketing kampagner.<br />

Salg, for at kunne følge op på kunder, der har udtrykt interesse for at købe et produkt. Opfølgning<br />

på alle muligheder for et salg er vigtig for virksomheden.<br />

Service <strong>og</strong> support til de instrumenter, som kunder har købt. I den forbindelse bruges det både til<br />

at håndterer fejl hos en kunde, <strong>og</strong> <strong>og</strong>så til versionsstyring af de enkelte instrumenter. I tilfælde af<br />

fejl kontakter kunden det lokale salgsselskab, som opretter en support incident på kundens<br />

instrument i Pivotal. Service afdelingen i Danmark håndterer alle support incidents. Hvis det er en<br />

generel fejl, slår man op i Pivotal, <strong>og</strong> finder alle de kunder, som har et tilsvarende instrument.<br />

Derefter sendes en besked ud til alle salgsselskaberne, om at sende en tekniker ud <strong>og</strong> opdatere<br />

kundernes instrumenter.<br />

Side 9


Pivotal er bygget op med en master server i Danmark <strong>og</strong> en masse satellit servere rundt om i<br />

verden. Det skal medvirke til at sikre at salgsselskaber over hele verden får rimelig hurtig adgang<br />

til Pivotal, <strong>og</strong> at serverne ikke overbelastes. Data bliver synkroniseret <strong>mellem</strong> master serveren <strong>og</strong><br />

de enkelte satellit servere rundt om i verden. Man kan køre Pivotal enten direkte koblet op til<br />

master server via en Citrix klient eller med et mobilt system mod en satellit server. Når man kører<br />

med et mobilt system har man et udsnit af dataene i en lokal database på ens computer, <strong>og</strong> når<br />

man er l<strong>og</strong>get på internettet, synkroniserer ens pc data til serveren via en web service.<br />

Når man opretter en tabel i Pivotal kan man vælge, om den skal blive synkroniseret eller ej. Der er<br />

d<strong>og</strong> den begrænsning, at hvis et pr<strong>og</strong>ram udenfor Pivotal opdaterer data i en tabel, så bliver<br />

denne tabel ikke længere synkroniseret videre. Det er derfor nødvendigt at lave flere tabeller,<br />

hvor man kan opdaterer i én tabel, <strong>og</strong> derefter bruger en Pivotal agent til at opdatere dataene i en<br />

anden tabel, som så kan synkronisere videre.<br />

Side 10


Pivotal Tabel design / konstruktion<br />

Alt i Pivotal er lavet, så man skal klikke sig rundt i forskellige vinduer for at lave nye tabeller, forms,<br />

queries <strong>og</strong> agenter. Det gør, at det umiddelbart er let at lave ting i Pivotal, at komme i gang. Men<br />

det er meget svært at lave komplicerede ting, fordi Pivotal agenterne hurtigt gør det uoverskueligt<br />

<strong>og</strong> overblikket for nemt mistes.<br />

Tabel vindue<br />

Her kan man oprette nye felter <strong>og</strong> beslutte om tabellen skal synkronisere. Man angiver <strong>og</strong>så her<br />

hvilken form, der skal være tilknyttet til tabellen, <strong>og</strong> om folk skal have mulighed for at lave queries<br />

på tabellen. Endvidere kan man lave indexes <strong>og</strong> lister, som brugerne kan anvende i deres oversigt,<br />

så de ikke behøver at lave queries hver gang.<br />

Side 11


Under Add Table Field, kan man angive feltnavn, type, om det er en fremmednøgle <strong>og</strong> man kan<br />

sætte en default formular, som indsætter en default værdi i et felt, hvis brugeren ikke selv<br />

indtaster n<strong>og</strong>et.<br />

Side 12


Pivotal Forms<br />

De skærmbilleder brugerne ser i Pivotal, kaldes forms.<br />

Forms opbygges af grupper. Inden i grupperne kan man have primære <strong>og</strong> sekundære felter.<br />

Primære felter stammer fra den tabel, man laver formen over. Sekundære felter kommer fra<br />

andre tabeller <strong>og</strong> kræver en fremmednøgle relation.<br />

Man placerer henholdsvis felterne <strong>og</strong> grupperne ved at angive, hvordan de skal ligge i forhold til<br />

henholdsvis andre felter / grupper rundt om.<br />

Sekundære felter bliver altid vist som rækker, de kan ikke vises som enkelte felter.<br />

Side 13


Pivotal agenter<br />

Den eneste måde man kan kode i Pivotal er ved at arbejde med såkaldte ”agenter”. Agenter er<br />

opbygget af komponenter, <strong>og</strong> flowet vises med pile.<br />

Hver komponent kan forskellige ting <strong>og</strong> virker på forskellige niveauer. ”Form komponenten” kan<br />

kun arbejde med felter fra én form. En ”record komponent” kan arbejde med al data fra en tabel,<br />

<strong>og</strong> kan derudover baseres på queries.<br />

Generelt om komponenter. Hvis en komponent gennemløbes uden fejl, fortsætter agenten videre<br />

fra komponentens venstre ben. Hvis der er fejl, går den ud fra komponentens højre ben. Hvis et<br />

ben ikke er forbundet videre, vil agenten stoppe.<br />

Hvis man bruger en test komponent, vil den gå ud ad venstre ben, hvis kriteriet var sandt. Hvis<br />

kriteriet er falsk, går den videre ud ad højre ben.<br />

For en gennemgang af de mest brugte komponenter se bilag.<br />

Løkker i agenter<br />

Løkker er nødvendige, når man vil kalde den samme komponent flere gange. Det kan være nyttigt,<br />

hvis man for eksempel vil opdatere 1000 records i en agent. Man laver en record komponent, der<br />

udfører de ønskede opdateringer. Den kan d<strong>og</strong> kun arbejde på én record af gangen, <strong>og</strong> når den er<br />

færdig, går agenten videre til en anden komponent. Derfor bliver man nødt til at kalde den samme<br />

komponent flere gange, indtil der ikke er flere records, der skal opdateres. Konventionen i Pivotal<br />

er at lave en shortcut til record komponenten, som det kan ses i figuren ovenover.<br />

Side 14


Herefter sætter man record komponenten til at finde de records, der skal opdateres. I shortcut<br />

komponenten laver man de ønskede opdateringer. Når record komponenten ikke finder flere<br />

records, brydes løkken, <strong>og</strong> agenten kører videre ad record komponentens højre ben.<br />

Silverpop<br />

Man l<strong>og</strong>ger ind på Silverpop via deres hjemmeside <strong>og</strong> kan derfra oprette lister / tabeller <strong>og</strong><br />

relatere dem til hinanden. Der er <strong>og</strong>så der, man kan lave rapporter, queries samt lave <strong>og</strong> udsende<br />

e-mail kampagner.<br />

En lidt speciel ting i Silverpop er, at man ikke kan forbinde relational tables med hinanden. De kan<br />

kun forbindes til en liste, <strong>og</strong> ikke til andre tabeller. Listen kan forbindes med alle de tabeller, man<br />

har lyst til.<br />

Man skal være opmærksom på, at email adressen er et påkrævet felt i Silverpop, som skal være<br />

unikt for hver record, for at man kan lave email kampagner.<br />

Side 15


Man kan desuden <strong>og</strong>så oploade til Silverpop via FTP. Der skal man oploade 2 filer. En CSV fil med<br />

data <strong>og</strong> en XML fil med instruktioner. I XML filen kan man specificere om man vil oploade data <strong>og</strong><br />

man kan <strong>og</strong>så samtidig oprette tabeller, <strong>og</strong> linke tabeller. Man har <strong>og</strong>så mulighed for at angive at<br />

man vil slette de records hos Silverpop, som matcher med dem, man oploader i en CSV fil.<br />

Data der skal sendes fra Pivotal til Silverpop<br />

Marketing afdelingen vil have at følgende felter bliver synkroniseret fra Pivotal til Silverpop.<br />

Company ID, Company Name, Contact ID, First Name, Last Name, Job Title, Country, E-mail,<br />

FOSS RSM, First Name, Last Name, Job Title, Company, E-mail, Phone, fra contact tabellen.<br />

RSM står for Regional Sales Manager. Det er den salgsmanager, der er ansvarlig for det land /<br />

område, som kunden bor i.<br />

Desuden er man interesseret i interest areas for kunden. Det er de områder, som kunden har<br />

ønsket at modtage informationer om. For contacts er det <strong>og</strong>så interessant at vide hvilke<br />

produkter, kunden har købt. Derfor skal products <strong>og</strong> den product group de tilhører <strong>og</strong>så<br />

synkroniseres til Silverpop.<br />

Man er interesseret i de tilsvarende felter for leads, <strong>og</strong>så i deres interest areas.<br />

Data fra Silverpop til Pivotal<br />

Informationen fra Silverpop skal tilføjes til tabellen Rn_appointments <strong>og</strong> vises på skærmbilledet<br />

Marketing Activity.<br />

Fra marketing er man interesseret i at få følgende informationer overført fra Silverpop til Pivotal.<br />

Navnet <strong>og</strong> tidspunktet for marketing kampagnen. Om kunden åbnede e-mailen <strong>og</strong> klikkede på<br />

n<strong>og</strong>le links. Om e-mailen blev Soft bounced eller Hard bounced. Hvis e-mailen blev åbnet eller<br />

bounced skal datoen for den hændelse <strong>og</strong>så gemmes.<br />

En e-mail er Hard bounced, når det er en ugyldig e-mail adresse, man har sendt til. Soft bounced<br />

kan betyde flere ting. Det kan f.eks. være at man har sendt til en overfyldt e-mail, som derfor ikke<br />

kan modtage flere e-mails.<br />

Side 16


Design<br />

Tabel opbygning i Silverpop.<br />

Datastrukturen i Silverpop bygges op på følgende måde. En hovedliste <strong>og</strong> 3 tabeller.<br />

Listen indeholder alle oplysninger om den enkelte kunde. Det kan være både en contact eller et<br />

lead. Til listen er tilknyttet 3 tabeller. Products indeholder alle de apparater, som den pågældende<br />

kunde har købt. Interest area er de områder, kunden har udtrykt interesse for. Preferred Com<br />

Methods indeholder information om de kontaktformer, kunden ønsker. Det kan være at kunden<br />

gerne vil have nyheder på e-mail <strong>og</strong> <strong>og</strong>så gerne vil have In Focus magasinet tilsendt i papir form.<br />

En kunde kan have flere products, interest areas <strong>og</strong> preferred com methods.<br />

Nye tabeller i Pivotal<br />

For at kunne lave synkronisering indenfor <strong>pivotal</strong>, er der brug for 3 nye tabeller i Pivotal.<br />

Silverpop in Indeholder alle data, der kommer fra Silverpop.<br />

Silverpop out Indeholder data, der skal til Silverpop.<br />

Silverpop temp Indeholder contact <strong>og</strong> lead id for alle de records, der skal synkroniseres fra<br />

Pivotal til Silverpop.<br />

Side 17


Synkronisering af data fra Pivotal til Silverpop<br />

Første skridt er opsamling af data fra Pivotal. Det vil sige alle contacts <strong>og</strong> leads, der lige er blevet<br />

oprettet i Pivotal, <strong>og</strong> derfor ikke er i Silverpop endnu. Derudover alle de contacts <strong>og</strong> leads, der er<br />

blevet ændret siden sidste synkronisering.<br />

Disse records skal markeres eller gemmes i en særskilt tabel. Dermed ved man hvilke records, der<br />

skal sendes til Silverpop ved den næste synkronisering. Fra marketings side vil man gerne have at<br />

data bliver synkroniseret en gang i døgnet. På den måde forstyrrer man færrest brugere af Pivotal.<br />

Der går lidt tid <strong>mellem</strong> hver marketing kampagne, så er det rigeligt med een synkronisering i<br />

døgnet.<br />

Der skal <strong>og</strong>så holdes styr på den enkelte record. Er der tale om en kunde, der har afmeldt e-mails<br />

fra FOSS, skal kundens record slettes fra hovedlisten i Silverpop.<br />

Fra marketing vil man gerne have muligheden for at kunne genopskrive kunder til marketing emails,<br />

hvis kunden ønsker det. Man skal samtidig kunne se i <strong>pivotal</strong> om en contact eller lead har<br />

ønsket at afmelde <strong>og</strong> genopskrive til marketing e-mails.<br />

Man kunne vælge at lade Silverpop styre unsubscribes. Hvis man t<strong>og</strong> den løsning, ville der være et<br />

link i hver e-mail, hvor kunden kunne ind på en Silverpop hjemmeside <strong>og</strong> afmelde sig der. Fra<br />

Silverpops side har man valgt, at hvis en kunde unsubscriber via deres system, kommer kundens email<br />

adresse ind på en afmeldt liste, <strong>og</strong> derefter kan man aldrig sende e-mails til ham igen. Derfor<br />

ønsker marketing, at afmeldinger bliver håndteret i Pivotal.<br />

Dette betyder, at alle de kunder, som er i Silverpop, ønsker at modtage e-mails. Hvis en kunde<br />

afmelder sig, skal kundens oplysninger slettes fra Silverpop.<br />

Når der er styr på alle de records, der skal oploades, skal dataene læses fra Pivotal <strong>og</strong> sendes til<br />

Silverpop. Det er umuligt via agenter i Pivotal. Der skal et pr<strong>og</strong>ram udefra for at gøre det. Valget<br />

faldt på C#. Det er et naturligt valg, fordi det er det spr<strong>og</strong>, man bruger i FOSS i andre<br />

sammenhæng. Det bruges til hjemmesiden <strong>og</strong> er blevet brugt til andre <strong>integration</strong>sprojekter.<br />

Til sidst skal dataen fra Pivotal lægges i en CSV fil <strong>og</strong> sammen med en XML fil oploades til<br />

Silverpop.<br />

Side 18


Synkronisering fra Silverpop til Pivotal<br />

For at få data fra Silverpop, skal man sende en XML fil med en export request. Silverpop genererer<br />

derefter en CSV fil. Den indeholder alle de records, som er blevet bounced, åbnet eller andet,<br />

siden sidste gang man lavede en export fra Silverpop.<br />

Filen bliver pakket med zip, <strong>og</strong> lagt på Silverpops server under ftp mappen.<br />

Den skal downloades <strong>og</strong> pakkes ud.<br />

Dataene skal derefter lægges ind i Silverpop in tabellen i Pivotal. Alt dette skal ske i C#<br />

pr<strong>og</strong>rammet.<br />

I Pivotal skal der laves en agent, der læser dataene fra Silverpop in tabellen <strong>og</strong> gemmer dataene i<br />

tabellen Rn_appointments. Dermed kan data fra Rn_appointments blive synkroniseret igennem<br />

Pivotal til resten af verden.<br />

Data fra Rn_appointments vises på skærmbilledet Marketing Activity.<br />

Unsubscribe<br />

Der skal laves en agent, der kan afmelde contacts <strong>og</strong> leads. Den gælder i de situationer, hvor en<br />

kunde ikke ønsker at modtage n<strong>og</strong>en information fra FOSS overhovedet.<br />

Den skal køres, når en superbruger sætter hak i en tjekboks. Den skal vises på skærmbillederne for<br />

contact, lead <strong>og</strong> prospects.<br />

Når den bliver kørt, skal den i kommentarerne skrive navnet på den superbruger, der har afmeldt<br />

kunden samt tidspunkt for afmeldelsen.<br />

Den skal fjerne alle kundens Preferred com methods <strong>og</strong> sørge for, at kunden bliver fjernet fra<br />

Silverpop.<br />

Det skal <strong>og</strong>så være muligt at genopskrive kunden igen via denne agent.<br />

Side 19


Implementering<br />

Unsubscribe agent<br />

Agenten bliver kørt enten fra contact, lead eller prospects formen.<br />

Man er ikke interesseret i, at n<strong>og</strong>en kommer til at afmelde en kunde ved en fejl. Derfor har man<br />

valgt, at det kun er superbrugere, der har lov til at afmelde kunder.<br />

Det første agenten undersøger, er om brugeren har de nødvendige rettigheder, dvs. er<br />

superbruger.<br />

Hvis det er tilfældet, går agenten videre <strong>og</strong> undersøger, om kunden er blevet unsubscribed før.<br />

Det vil sige, om unsubscribed boolean er sat til sandt (check unsubscribe flag). Hvis den er sandt,<br />

bliver brugeren bedt om at bekræfte, at kunden skal kunne modtage e-mails / magasiner igen.<br />

Flaget bliver fjernet fra unsubscribe, <strong>og</strong> brugerens navn <strong>og</strong> tidspunkt gemmes under noter på<br />

kundens record (unset unsubscribe). Derefter opdateres formen, så det igen er muligt at tilføje,<br />

om kunden ønsker at modtage skrivelser.<br />

Side 20


Skal kunden afmeldes (no ved check unsubscribe flag), kommer der en meddelelse til brugeren.<br />

Når den er blevet bekræftet, sættes unsubscribe flaget til sandt. Derefter løbes alle kundens<br />

preferred com methods igennem, arkiveres, <strong>og</strong> listen slettes.<br />

Når der arkiveres en preferred com methods record betyder det at der oprettes en archive<br />

preferred com methods record. Den indeholder kunde record Id, preferred com method type,<br />

tidspunkt <strong>og</strong> om det er en tilføjelse eller sletning. Det vil sige, at det bliver arkiveret, når en kunde<br />

ønsker at modtage nyheds- <strong>og</strong> reklame e-mails <strong>og</strong> når han frabeder sig det. Dermed er der en<br />

komplet historik. Hvis en kunde klager over at modtage e-mails, kan man slå op i tabellen <strong>og</strong> se om<br />

han har tilmeldt sig <strong>og</strong> evt. afmeldt sig igen.<br />

Hvis der har været n<strong>og</strong>le opdateringer af kundens stamdata, vil der være blevet oprettet n<strong>og</strong>le<br />

records i Silverpop temp. Disse bliver fjernet fra tabellen (remove other <strong>silverpop</strong> temp records).<br />

Der bliver lavet en ny record i Silverpop temp med kundens record id, e-mail adresse <strong>og</strong> deletion<br />

sat til sand. Dermed bliver kundens oplysninger slettet fra Silverpop efter næste synkronisering, <strong>og</strong><br />

kunden vil efterfølgende ikke modtage flere e-mails.<br />

Til sidst bliver formen opdateret. Det er derefter ikke muligt at sætte kunden til at modtage<br />

skrivelser.<br />

Side 21


Data fra Pivotal til Silverpop agent<br />

Agenten kører en gang i døgnet ved midnat.<br />

Agenten læser fra Silverpop temp tabellen efter en query, som tester om agent_handled er falsk.<br />

Derefter bliver der testet, om det er en deletion. Hvis det er tilfældet, bliver der oprettet en record<br />

i Silverpop out tabellen, med contact / lead Id, E-mail, company Id <strong>og</strong> deletion sat til sandt. Hvis<br />

det ikke er en deletion, bliver der testet, om det er et lead (Is this a lead). Det efterfølgende er ens<br />

for contacts <strong>og</strong> leads. Den eneste forskel er, at det drejer sig om forskellige tabeller.<br />

Først bruges Id’et fra Silverpop temp til at hente den originale record frem. Der undersøges om<br />

den record har en e-mail adresse. Det er kun interessant at synkroniserer kunder med e-mail<br />

adresser til Silverpop.<br />

Derefter oprettes en record i Silverpop out tabellen. I den record gemmes kunde <strong>og</strong> RSM<br />

oplysninger.<br />

Side 22


Bagefter bliver Set Business area kørt. Business area er det samme som interest area. Den finder<br />

alle kundens interest areas, <strong>og</strong> gemmer dem i Silverpop out recorden. Til sidst slettes den<br />

nuværende record i Silverpop temp, <strong>og</strong> agenten læser den næste record fra Silverpop temp.<br />

Hvis der på n<strong>og</strong>et tidspunkt opstår en fejl, vil der blive sendt en mail til en Pivotal pr<strong>og</strong>rammør.<br />

Silverpop temp recorden vil blive sat til agent_handled sandt, <strong>og</strong> agenten vil springe den record<br />

over <strong>og</strong> tage den næste. Når der ikke er flere records i Silverpop temp, går agenten i sleep mode.<br />

Den vågner op igen ved midnat, <strong>og</strong> ser efter flere records i Silverpop temp.<br />

Side 23


Data fra Silverpop til Pivotal agent<br />

Agenten kører altid, men er sat til at sove i 2 minutter, når der ikke er flere records i Silverpop in.<br />

Agenten læser fra Silverpop in tabellen efter en query. Den query frasorterer records, hvor<br />

agent_handled er sat til sandt.<br />

I test komponenten tester agenten, om det er en contact. Det er enten en contact eller et lead.<br />

Derefter opdateres den pågældende record, hvis e-mailen til kunden bounced.<br />

Bagefter laves en ny record i Marketing Activity. Den bliver udfyldt med navnet på marketing email<br />

kampagnen <strong>og</strong> tidspunkt.<br />

Hvis der er blevet klikket på n<strong>og</strong>le links, eller e-mailen er blevet soft eller hard bounced, bliver<br />

dette tilføjet under noten på recorden. Derudover bliver datoen for disse events <strong>og</strong>så gemt på<br />

recorden.<br />

Til sidst slettes Silverpop in recorden, <strong>og</strong> agenten starter forfra med den næste record fra tabellen.<br />

Side 24


Hvis der opstår fejl undervejs, bliver fejl recorden gemt med agent_handled sat til sandt, <strong>og</strong> der<br />

bliver sendt en mail til en Pivotal pr<strong>og</strong>rammør. Agenten kan derefter køre videre med en anden<br />

record.<br />

Data fra Pivotal til Silverpop C# kode<br />

ConsoleApplication1 / SilverpopIntegration<br />

Denne kode læser data fra Pivotal i tabellen Siverpop out, behandler det <strong>og</strong> sender det til<br />

Silverpop.<br />

Der er App.config, som indeholder praktiske oplysninger, så som server adresse, user name,<br />

passwords <strong>og</strong> e-mail adresser. Dermed er det nemmere at finde, når man skal opdatere n<strong>og</strong>le af<br />

disse oplysninger.<br />

Selve C# pr<strong>og</strong>rammet består af en række filer. De er delt op så hver fil, er en separat funktion.<br />

Pr<strong>og</strong>ram.cs Hovedpr<strong>og</strong>rammet, styrer processen <strong>og</strong> kalder<br />

funktioner fra de andre filer.<br />

L<strong>og</strong>.cs Bruges igennem det meste af pr<strong>og</strong>rammet til at l<strong>og</strong>ge<br />

events til en l<strong>og</strong> fil.<br />

ErrorMailGenerator.cs Sender en e-mail ud i tilfælde af fejl.<br />

CsvFileGenerator.cs Funktioner til at generere csv filer.<br />

ByteConvertor.cs Bruges til at generere <strong>og</strong> håndtere record ids til Pivotal.<br />

Bemærk! Jeg har ikke lavet denne fil.<br />

SilverpopAPI.cs Bruges til at l<strong>og</strong>ge på <strong>og</strong> ud af Silverpop.<br />

SilverpopFTP.cs Bruges til at sende <strong>og</strong> modtage data fra /til Silverpop<br />

TransactionalDatabase.cs Bruges til alle database kald.<br />

Pr<strong>og</strong>ram forløb:<br />

Først hentes de records ud fra Pivotal, som skal slettes i Silverpop. Deres Id bliver konverteret ved<br />

hjælp af ByteConvertor.cs. Det er på grund af at Ids i Pivotal, er i et meget specielt format. Der<br />

Side 25


findes ingen dokumentation om det <strong>og</strong> de kunne ikke forklare det. Derfor valgte jeg at bruge den<br />

fil, de plejer at bruge, ByteConvertor.cs. Jeg har derfor ikke selv lavet denne fil.<br />

Efter konverteringen bliver XML filen oploadet til Silverpop, som sletter de pågældende records.<br />

Derefter indlæses de records fra Pivotal, som skal opdateres i Silverpop. Der bliver genereret 4<br />

XML filer: MainList.xml, pref_comm.xml, products.xml <strong>og</strong> ba.xml.<br />

XML filerne indeholder kommandoer til Silverpop. Hvor der bliver specificeret at de skal<br />

add_and_update, dvs. opdatere record, hvis den findes i forvejen ellers lav en ny. Derudover<br />

indeholder de navnet <strong>og</strong> id på den tabel eller liste, der skal opdateres i Silverpop. Efter det bliver<br />

kolonnerne listet <strong>og</strong> deres placering i CSV filen angivet.<br />

Til sidst bliver XML <strong>og</strong> CSV filerne oploadet til Silverpop. Hver opload bliver kørt i hver sin tråd.<br />

Dermed skal de ikke vente på at den foregående opload er blevet færdigbehandlet i Silverpop.<br />

Data fra Silverpop til Pivotal C# kode<br />

Return Silverpop Data<br />

Denne kode henter data fra Silverpop, behandler det <strong>og</strong> sender det til Pivotal til tabellen Silverpop<br />

in.<br />

Der er ligesom i den foregående kode en App.config fil, som indeholder praktiske oplysninger.<br />

C# pr<strong>og</strong>rammet er <strong>og</strong>så bygget op på samme måde som før.<br />

Pr<strong>og</strong>ram.cs Hovedpr<strong>og</strong>rammet, styrer processen <strong>og</strong> kalder<br />

funktioner fra de andre filer.<br />

L<strong>og</strong>.cs Bruges igennem det meste af pr<strong>og</strong>rammet til at l<strong>og</strong>ge<br />

events til en l<strong>og</strong> fil.<br />

ErrorMailGenerator.cs Sender en e-mail ud i tilfælde af fejl.<br />

CsvParser.cs Funktioner til at læse en csv file <strong>og</strong> indlæse i et<br />

DataTable.<br />

ByteConvertor.cs Bruges til at generere <strong>og</strong> håndtere record ids til Pivotal.<br />

Bemærk! Jeg har ikke lavet denne fil.<br />

SilverpopAPI.cs Bruges til at l<strong>og</strong>ge på <strong>og</strong> ud af Silverpop.<br />

SilverpopFTP.cs Bruges til at sende <strong>og</strong> modtage data fra /til Silverpop<br />

Side 26


TransactionalDatabase.cs Bruges til alle database kald.<br />

Pr<strong>og</strong>ram forløb:<br />

Først sendes en export request til Silverpop. Silverpop genererer en zip fil, der indeholder en CSV<br />

fil, med data. Filen downloades via FTP.<br />

CSV filen pakkes ud <strong>og</strong> zip filen slettes, for at begrænse mængden af filer på serveren.<br />

Herefter er det meningen at CSV filen skal indlæses i pr<strong>og</strong>rammet <strong>og</strong> bagefter skrives til Silverpop<br />

in tabellen i Pivotal.<br />

Dette virker på nuværende tidspunkt ikke. Der er en fejl, når CSV filen bliver indlæst til et<br />

DataTable. Den returnerer et tomt DataTable <strong>og</strong> der er derfor intet at indlæse til Pivotal.<br />

Side 27


Test<br />

Jeg har lavet manuel testning af de forskellige pr<strong>og</strong>ramdele. Jeg har testet ved at løbende at lave<br />

udskrifter til skærmen både i Pivotal <strong>og</strong> i C# koden. Det er ikke muligt at teste på andre måder i<br />

Pivotal. Derudover har jeg kørt pr<strong>og</strong>rammet <strong>og</strong> kontrolleret at data blev flyttet rigtigt fra tabel til<br />

tabel <strong>og</strong> fra Pivotal til Silverpop.<br />

Alle agenter er testet <strong>og</strong> virker. De stedet hvor jeg ikke har haft data fra C# koden, har jeg manuelt<br />

lagt data ind i tabellerne.<br />

Jeg har haft brugere fra marketing afdelingen til at teste at dataene blev synkroniseret fra Pivotal<br />

til Silverpop.<br />

De kunne bekræfte at dataene kom frem til Silverpop, men derudover var de meget usikre på<br />

hvordan man fandt rundt i Pivotal <strong>og</strong> hvordan man brugte Silverpop.<br />

Som følge af testen ville de gerne have at Interest Areas <strong>og</strong>så kom hen i MainList i Silverpop.<br />

Dermed kunne de bedre overskue at teste i Silverpop.<br />

Jeg har derfor ændret koden så alle interest areas, <strong>og</strong>så ligger i MainList som booleans.<br />

Jeg har undervejs mens jeg lavede pr<strong>og</strong>rammerede kun arbejdet på test servere. I <strong>og</strong> med at<br />

marketing sagde god for synkroniseringsdelen fra Pivotal til Silverpop, er denne del blevet sat i<br />

produktion.<br />

Side 28


Konklusion<br />

Opfyldte krav i forhold til løsningen<br />

Synkronisering fra Pivotal til Silverpop. Denne del er lavet <strong>og</strong> testet, at det virker korrekt. Det er i<br />

produktion i dag.<br />

Synkronisering fra Silverpop til Pivotal. Denne del virker ikke. Fejlen begrænser sig d<strong>og</strong> til<br />

Csvparseren. Alle de andre komponenter i C# koden er blevet testet. Agenten, der skal håndterer<br />

dataene i Pivotal, er <strong>og</strong>så blevet testet <strong>og</strong> virker <strong>og</strong>så.<br />

Skærmbilleder <strong>og</strong> tabel opdateringer. Disse er implementeret.<br />

Unsubscribe <strong>og</strong> resubscribe funktion. Dette er <strong>og</strong>så implementeret. Det er <strong>og</strong>så testet at agenten<br />

får unsubscribet kunden, fjernet interest areas <strong>og</strong> at kunden bliver fjernet fra Silverpop efter<br />

næste synkronisering.<br />

Fleksibelt designet. Dette punkt vil jeg mene er opnået. Jeg har brugt få agenter, <strong>og</strong> disse er<br />

forsøgt at være overskuelige. C# koden er delt op i en række filer, med hver deres<br />

funktionsområde.<br />

Tilfredshed fra marketing afdelingen<br />

Marketing afdelingen er tilfreds med at data bliver synkroniseret til Silverpop. Dermed har de data<br />

klar <strong>og</strong> kan lave n<strong>og</strong>le testkampagner. At synkroniseringen ikke virker tilbage til Pivotal, er ikke et<br />

stort problem på nuværende tidspunkt. De har travlt med at rydde op i dataene i Pivotal. Der er en<br />

masse manuelt arbejde i at finde ud af, hvem man skal sende e-mails til, hvis flere kunder har den<br />

samme e-mail adresse.<br />

Benefit for forretningen<br />

Marketing afdelingen slipper for at manuelt skulle lave filer med 5-20000 records der skal manuelt<br />

importeres. Dermed spares der meget tid.<br />

Man har mulighed for at lave dynamiske e-mail kampagner. De rammer kunderne bedre med<br />

mere specifikt information til dem. Dermed burde det genererer mere salg.<br />

Fremtidig udvikling<br />

Synkroniseringen fra Silverpop til Pivotal skal gøres færdig.<br />

Der kommer givet mere, der skal laves om, når marketing begynder at bruge Silverpop <strong>og</strong> Pivotal.<br />

Jeg forventer at de både vil have mere data til <strong>og</strong> fra Silverpop. I Pivotal vil have mere data på<br />

contact <strong>og</strong> lead formene <strong>og</strong> en række queries. Dermed kan de begynde at måle effektiviteten af<br />

deres e-mail kampagner.<br />

Side 29


Bilag<br />

Pivotal Agenter<br />

Pivotal Agent Komponenter<br />

Navn Ikon Beskrivelse<br />

Test<br />

Wait<br />

Agent<br />

Import<br />

List<br />

Tester et udtryk <strong>og</strong> returnerer enten sandt eller false.<br />

Bruges til at sætte en agent til at vente i et stykke tid.<br />

Bruges til at indlejrer en agent i en anden agent.<br />

Komponenterne i den indlejrede agent kan bruge variablerne i<br />

hovedagenten. En indlejret agent bruges ofte til at skjule en del<br />

komponenter fra hovedagenten, <strong>og</strong> dermed skabe overblik.<br />

Bruges til at importerer data ind i Pivotal. Dataene skal ligge i en<br />

CSV fil.<br />

Lister bruges til at vise data i Pivotal, når queries returnerer<br />

mere end 1 record. Komponenten kan oprette, slette <strong>og</strong><br />

manipulere lister.<br />

Side 30


Form<br />

Record<br />

Send<br />

Variable<br />

Move<br />

Call<br />

Kan læse <strong>og</strong> skrive til felter på en Pivotal Form. Kan <strong>og</strong>så<br />

manipulere de enkelte felter, f.eks. lave n<strong>og</strong>le af dem read only.<br />

Kan læse <strong>og</strong> skrive til en record i databasen. Kan baseres på en<br />

query, så man kun får en delmængde af dataene. Derudover<br />

den <strong>og</strong>så manipulere felter på den enkelte record.<br />

Sender en e-mail ud. Man kan sætte modtager, cc, cb, titel <strong>og</strong><br />

besked i komponenten.<br />

Bruges som en almindelig variable, til at gemme en værdi<br />

midlertidig.<br />

Bruges til at flytte data, f.eks. til <strong>og</strong> fra en variable.<br />

Man kan køre andre agent med denne komponent. Agenterne<br />

kan ikke dele variable, men man kan overfører argumenter når<br />

man kalder call.<br />

Side 31


Prompt<br />

Note<br />

Return Silverpop Data C#<br />

App.config<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

Bruges til at skrive tekst <strong>og</strong> evt. variabler ud på skærmen til<br />

brugeren. Bruges <strong>og</strong>så når brugeren skal tage stilling til n<strong>og</strong>et.<br />

Kan skrive en besked ud på skærmen til brugeren.<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

Side 32


ByteConvertor.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public static class ByteConvertor<br />

{<br />

/// <br />

/// Converts an decimal id into an bytearray id<br />

/// <br />

/// The decimal representation of the id<br />

/// The Bytearray representation of the id.<br />

public static byte[] ConvertToFossBytes(string id)<br />

{<br />

byte[] converted = new byte[8];<br />

string binstring = "";<br />

}<br />

if (id.Contains("-"))<br />

{<br />

string[] num = id.Split('-');<br />

binstring = DecimalToBase(Convert.ToInt32(num[0]), 2).PadLeft(24, '0');<br />

binstring += DecimalToBase(Convert.ToInt32(num[1]), 2).PadLeft(40, '0');<br />

}<br />

else<br />

{<br />

binstring = DecimalToBase(Convert.ToInt32(id), 2).PadLeft(64, '0');<br />

}<br />

for (int i = 0; i < converted.Length; i++)<br />

{<br />

converted[i] = (byte)BinToDecimal(binstring.Substring(i * 8, 8));<br />

}<br />

return converted;<br />

/// <br />

/// Converts a bytearray representation of an id into a decimal representation of the id<br />

/// <br />

/// bytearray representation of the id<br />

/// the decimal representation of the id<br />

public static string ConvertFossBytes(byte[] workid)<br />

{<br />

string[] BinStringArr = new string[workid.Length];<br />

for (int i = 0; i < workid.Length; i++)<br />

{<br />

BinStringArr[i] = DecimalToBase(workid[i], 2).PadLeft(8, '0');<br />

}<br />

string Head = "";<br />

string Tail = "";<br />

Head = BinStringArr[0];<br />

Head += BinStringArr[1];<br />

Head += BinStringArr[2];<br />

Tail = BinStringArr[3];<br />

Tail += BinStringArr[4];<br />

Tail += BinStringArr[5];<br />

Tail += BinStringArr[6];<br />

Tail += BinStringArr[7];<br />

string HeadOut = BinToDecimal(Head).ToString();<br />

string TailOut = BinToDecimal(Tail).ToString();<br />

Side 33


}<br />

}<br />

}<br />

if (HeadOut == "0")<br />

return TailOut;<br />

else<br />

return HeadOut + "-" + TailOut;<br />

/// <br />

/// Converts a decimal representation of a number into the "base" representation of the number<br />

/// <br />

/// The decimal number<br />

/// The base that the the decimal should be converted into<br />

/// The converted number<br />

public static string DecimalToBase(int iDec, int numbase)<br />

{<br />

string strBin = "";<br />

int[] result = new int[32];<br />

int MaxBit = 32;<br />

for (; iDec > 0; iDec /= numbase)<br />

{<br />

int rem = iDec % numbase;<br />

result[--MaxBit] = rem;<br />

}<br />

for (int i = 0; i < result.Length; i++)<br />

{<br />

strBin += result.GetValue(i);<br />

}<br />

strBin = strBin.TrimStart(new char[] { '0' });<br />

return strBin;<br />

}<br />

/// <br />

/// Converts from binary to decimal<br />

/// <br />

/// binary representation of the number<br />

/// the decimal representation of the number <br />

public static int BinToDecimal(string sBase)<br />

{<br />

int dec = 0;<br />

int bit = 0;<br />

for (int i = sBase.Length - 1; i >= 0; i--)<br />

{<br />

if ("1" == sBase[i].ToString())<br />

{<br />

dec += (int)Math.Pow(2, bit);<br />

}<br />

bit++;<br />

}<br />

return dec;<br />

}<br />

Side 34


CsvParser.cs<br />

using System;<br />

using System.Collections;<br />

using System.Text;<br />

using System.Data;<br />

using System.IO;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public static class CsvParser<br />

{<br />

public static DataTable Parse(string data, bool headers)<br />

{<br />

return Parse(new StringReader(data), headers);<br />

}<br />

public static DataTable Parse(string data)<br />

{<br />

return Parse(new StringReader(data));<br />

}<br />

public static DataTable Parse(TextReader stream)<br />

{<br />

return Parse(stream, false);<br />

}<br />

/// <br />

/// Parses a CSV file and returns the data in a DataTable.<br />

/// <br />

/// The Stream containing the CSV contents.<br />

/// True if the file contains column headers.<br />

/// Returns the CSV data in the form of a DataTable.<br />

public static DataTable Parse(TextReader stream, bool headers)<br />

{<br />

DataTable table = new DataTable();<br />

CsvStream csv = new CsvStream(stream);<br />

string[] row = csv.GetNextRow();<br />

if (row == null)<br />

return null;<br />

if (headers)<br />

{<br />

foreach (string header in row)<br />

{<br />

if (header != null && header.Length > 0 && !table.Columns.Contains(header))<br />

table.Columns.Add(header, typeof(string));<br />

else<br />

table.Columns.Add(GetNextColumnHeader(table), typeof(string));<br />

}<br />

row = csv.GetNextRow();<br />

}<br />

while (row != null)<br />

{<br />

while (row.Length > table.Columns.Count)<br />

table.Columns.Add(GetNextColumnHeader(table), typeof(string));<br />

table.Rows.Add(row);<br />

row = csv.GetNextRow();<br />

}<br />

return table;<br />

}<br />

private static string GetNextColumnHeader(DataTable table)<br />

{<br />

int c = 1;<br />

while (true)<br />

{<br />

string h = "Column" + c++;<br />

if (!table.Columns.Contains(h))<br />

return h;<br />

Side 35


}<br />

}<br />

private class CsvStream<br />

{<br />

private TextReader stream;<br />

public CsvStream(TextReader s)<br />

{<br />

stream = s;<br />

}<br />

public string[] GetNextRow()<br />

{<br />

ArrayList row = new ArrayList();<br />

while (true)<br />

{<br />

string item = GetNextItem();<br />

if (item == null)<br />

return row.Count == 0 ? null : (string[])row.ToArray(typeof(string));<br />

row.Add(item);<br />

}<br />

}<br />

private bool EOS = false;<br />

private bool EOL = false;<br />

private string GetNextItem()<br />

{<br />

if (EOL)<br />

{<br />

// previous item was last in line, start new line<br />

EOL = false;<br />

return null;<br />

}<br />

bool quoted = false;<br />

bool predata = true;<br />

bool postdata = false;<br />

StringBuilder item = new StringBuilder();<br />

while (true)<br />

{<br />

char c = GetNextChar(true);<br />

if (EOS)<br />

return item.Length > 0 ? item.ToString() : null;<br />

if ((postdata || !quoted) && c == ',')<br />

// end of item, return<br />

return item.ToString();<br />

if ((predata || postdata || !quoted) && (c == '\x0A' || c == '\x0D'))<br />

{<br />

EOL = true;<br />

if (c == '\x0D' && GetNextChar(false) == '\x0A')<br />

GetNextChar(true);<br />

return item.ToString();<br />

}<br />

if (predata && c == ' ')<br />

continue;<br />

if (predata && c == '"')<br />

{<br />

quoted = true;<br />

predata = false;<br />

continue;<br />

}<br />

Side 36


}<br />

}<br />

}<br />

}<br />

if (predata)<br />

{<br />

predata = false;<br />

item.Append(c);<br />

continue;<br />

}<br />

if (c == '"' && quoted)<br />

{<br />

if (GetNextChar(false) == '"')<br />

item.Append(GetNextChar(true));<br />

else<br />

postdata = true;<br />

continue;<br />

}<br />

}<br />

item.Append(c);<br />

private char[] buffer = new char[4096];<br />

private int pos = 0;<br />

private int length = 0;<br />

private char GetNextChar(bool eat)<br />

{<br />

if (pos >= length)<br />

{<br />

length = stream.ReadBlock(buffer, 0, buffer.Length);<br />

if (length == 0)<br />

{<br />

EOS = true;<br />

return '\0';<br />

}<br />

pos = 0;<br />

}<br />

if (eat)<br />

return buffer[pos++];<br />

else<br />

return buffer[pos];<br />

}<br />

Side 37


ErrorMailGenerator.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.Net;<br />

using System.Net.Mail;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public static class ErrorMailGenerator<br />

{<br />

private static string recipient = System.Configuration.ConfigurationManager.AppSettings["ErrorMailRecipient"].ToString();<br />

}<br />

}<br />

public static string Recipient<br />

{<br />

get<br />

{<br />

return (recipient);<br />

}<br />

}<br />

public static void SendErrorMail(string body)<br />

{<br />

try<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending Error Mail to " + recipient);<br />

}<br />

//create the mail client<br />

SmtpClient sMail = new SmtpClient("mail.foss.dk");<br />

sMail.DeliveryMethod = SmtpDeliveryMethod.Network;<br />

//send the specified message<br />

sMail.Send("<strong>silverpop</strong>@foss.dk", recipient, "Error Generated by Silverpop Integration!", body);<br />

}<br />

catch(Exception ex)<br />

{<br />

//l<strong>og</strong> the error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending the Email FAILED - that's a bad thing, a very very bad thing");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(ex.Message);<br />

}<br />

Side 38


L<strong>og</strong>.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.IO;<br />

using System.Threading;<br />

namespace FOSS<br />

{<br />

public class L<strong>og</strong>Lock<br />

{<br />

private int locked;<br />

}<br />

public Boolean status<br />

{<br />

get<br />

{<br />

if (locked == 1) return true;<br />

else return false;<br />

}<br />

}<br />

public void LockL<strong>og</strong>()<br />

{<br />

while (Interlocked.CompareExchange(ref locked, 1, 0)==1)<br />

{<br />

Thread.Sleep(0);<br />

}<br />

}<br />

public void UnlockL<strong>og</strong>()<br />

{<br />

Interlocked.Exchange(ref locked, 0);<br />

}<br />

public static class MagicalL<strong>og</strong>File<br />

{<br />

private static string default_ = System.Configuration.ConfigurationManager.AppSettings["L<strong>og</strong>gingDefault"].ToString();<br />

private static int MaxFileSize = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["MaxFileSize"].ToString());<br />

private static L<strong>og</strong>Lock locked = new L<strong>og</strong>Lock();<br />

public static void MakeL<strong>og</strong>Entry(string msg)<br />

{<br />

MakeL<strong>og</strong>Entry(msg, default_);<br />

}<br />

public static void MakeL<strong>og</strong>Entry(string msg, string type)<br />

{<br />

try<br />

{<br />

locked.LockL<strong>og</strong>();<br />

if (type == "file")<br />

{<br />

L<strong>og</strong>MessageToFile(msg);<br />

CheckFileSize();<br />

}<br />

else<br />

{<br />

}<br />

}<br />

finally<br />

{<br />

locked.UnlockL<strong>og</strong>();<br />

}<br />

}<br />

private static string GetTempPath()<br />

Side 39


}<br />

}<br />

{<br />

}<br />

string path = System.Environment.CurrentDirectory;//System.Environment.GetEnvironmentVariable("TEMP");<br />

if (!path.EndsWith("\\")) path += "\\";<br />

return path;<br />

private static void L<strong>og</strong>MessageToFile(string msg)<br />

{<br />

System.IO.StreamWriter sw = System.IO.File.AppendText(<br />

GetTempPath() + "L<strong>og</strong>.txt");<br />

try<br />

{<br />

string l<strong>og</strong>Line = System.String.Format(<br />

"{0:G}: {1}.", System.DateTime.Now, msg);<br />

sw.WriteLine(l<strong>og</strong>Line);<br />

}<br />

finally<br />

{<br />

sw.Close();<br />

}<br />

}<br />

private static void CheckFileSize()<br />

{<br />

FileInfo fi = new FileInfo(GetTempPath() + "L<strong>og</strong>.txt");<br />

}<br />

if (fi.Length > MaxFileSize)<br />

{<br />

File.Move(GetTempPath() + "L<strong>og</strong>.txt", GetTempPath() + "L<strong>og</strong> - " + DateTime.Now.ToFileTimeUtc() + ".txt");<br />

File.Delete(GetTempPath() + "L<strong>og</strong>.txt");<br />

}<br />

Side 40


SilverpopAPI.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.Xml;<br />

using System.Configuration;<br />

using System.Web;<br />

using System.Net;<br />

using System.IO;<br />

using System.Threading;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public sealed class SilverpopAPI<br />

{<br />

private string sessionid;<br />

private string sessionencoding;<br />

private XmlDocument request;<br />

private XmlDocument response;<br />

private string jobstatus;<br />

private string url;<br />

private string username;<br />

private string password;<br />

private string jobid;<br />

private static string MainListId = System.Configuration.ConfigurationManager.AppSettings["MainListId"].ToString();<br />

public SilverpopAPI()<br />

{<br />

url = System.Configuration.ConfigurationManager.AppSettings["ApiUrl"].ToString();<br />

username = System.Configuration.ConfigurationManager.AppSettings["Username"].ToString();<br />

password = System.Configuration.ConfigurationManager.AppSettings["Password"].ToString();<br />

}<br />

request = new XmlDocument();<br />

response = new XmlDocument();<br />

public bool ApiL<strong>og</strong>in()<br />

{<br />

if (sessionid == null)<br />

{<br />

//create xml document<br />

request = BuildSilverpopWrapper();<br />

XmlElement L<strong>og</strong>in = request.CreateElement("L<strong>og</strong>in");<br />

request.GetElementsByTagName("Body")[0].AppendChild(L<strong>og</strong>in);<br />

//add username and password from app.config<br />

XmlElement usernameXML = request.CreateElement("USERNAME");<br />

usernameXML.InnerText = username;<br />

L<strong>og</strong>in.AppendChild(usernameXML);<br />

XmlElement passwordXML = request.CreateElement("PASSWORD");<br />

passwordXML.InnerText = password;<br />

L<strong>og</strong>in.AppendChild(passwordXML);<br />

//l<strong>og</strong>ging in<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("L<strong>og</strong>ging Into Silverpop API...");<br />

SubmitRequest();<br />

if (response == null) return (false);<br />

//parse the response to check if l<strong>og</strong>ged in, and get the sessionid<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "true")<br />

{<br />

sessionid = response.GetElementsByTagName("SESSIONID")[0].InnerText;<br />

sessionencoding = response.GetElementsByTagName("SESSION_ENCODING")[0].InnerText;<br />

Side 41


}<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Successfully l<strong>og</strong>ged in with sessionid: " + sessionid);<br />

return (true);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Failed to l<strong>og</strong>in to API :: " + response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

return (false);<br />

}<br />

else<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Already l<strong>og</strong>ged into Silverpop API");<br />

return (true);<br />

}<br />

public void ApiL<strong>og</strong>out()<br />

{<br />

if (sessionid == null)<br />

{<br />

//not l<strong>og</strong>ged in it seems<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Already l<strong>og</strong>ged out of API");<br />

//ensure variables are reset<br />

sessionencoding = null;<br />

}<br />

else<br />

{<br />

//create l<strong>og</strong>out request<br />

request = BuildSilverpopWrapper();<br />

}<br />

}<br />

XmlElement l<strong>og</strong>out = request.CreateElement("L<strong>og</strong>out");<br />

request.GetElementsByTagName("Body")[0].AppendChild(l<strong>og</strong>out);<br />

//l<strong>og</strong>ging out<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("L<strong>og</strong>ging out of API");<br />

//submit request and get response<br />

SubmitRequest();<br />

if (response == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Null response when attempting to l<strong>og</strong> out");<br />

}<br />

sessionencoding = null;<br />

sessionid = null;<br />

private XmlDocument BuildSilverpopWrapper()<br />

{<br />

XmlDocument xml = new XmlDocument();<br />

XmlDeclaration xmlDec = xml.CreateXmlDeclaration("1.0", "utf-8", null);<br />

XmlElement envelope = xml.CreateElement("Envelope");<br />

xml.InsertBefore(xmlDec, xml.DocumentElement);<br />

xml.AppendChild(envelope);<br />

XmlElement body = xml.CreateElement("Body");<br />

envelope.AppendChild(body);<br />

return (xml);<br />

}<br />

Side 42


public void ListImport(string list)<br />

{<br />

if (sessionid == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Attempted to make an API Request without being l<strong>og</strong>ged in!");<br />

}<br />

else<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement ImportList_ = request.CreateElement("ImportList");<br />

request.GetElementsByTagName("Body")[0].AppendChild(ImportList_);<br />

XmlElement MAP_FILE = request.CreateElement("MAP_FILE");<br />

MAP_FILE.InnerText = list + ".xml";<br />

ImportList_.AppendChild(MAP_FILE);<br />

XmlElement SOURCE_FILE = request.CreateElement("SOURCE_FILE");<br />

SOURCE_FILE.InnerText = list + ".csv";<br />

ImportList_.AppendChild(SOURCE_FILE);<br />

//l<strong>og</strong> the request<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending ListImport Request to Silverpop API for list: " + list);<br />

//make the request and get response<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! ListImport request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

jobid = response.GetElementsByTagName("JOB_ID")[0].InnerText;<br />

}<br />

}<br />

}<br />

public void TableImport(string list)<br />

{<br />

if (sessionid == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Attempted to make an API Request without being l<strong>og</strong>ged in!");<br />

}<br />

else<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement ImportTable_ = request.CreateElement("ImportTable");<br />

request.GetElementsByTagName("Body")[0].AppendChild(ImportTable_);<br />

XmlElement MAP_FILE = request.CreateElement("MAP_FILE");<br />

MAP_FILE.InnerText = list + ".xml";<br />

ImportTable_.AppendChild(MAP_FILE);<br />

XmlElement SOURCE_FILE = request.CreateElement("SOURCE_FILE");<br />

SOURCE_FILE.InnerText = list + ".csv";<br />

ImportTable_.AppendChild(SOURCE_FILE);<br />

//l<strong>og</strong> the request<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending TableImport Request to Silverpop API for list: " + list);<br />

Side 43


make the request and get response<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! TableImport request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

jobid = response.GetElementsByTagName("JOB_ID")[0].InnerText;<br />

}<br />

}<br />

}<br />

public void TrackingMetricExport()<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement TME = request.CreateElement("TrackingMetricExport");<br />

request.GetElementsByTagName("Body")[0].AppendChild(TME);<br />

XmlElement LIST_ID = request.CreateElement("LIST_ID");<br />

LIST_ID.InnerText = MainListId;<br />

TME.AppendChild(LIST_ID);<br />

XmlElement Move = request.CreateElement("MOVE_TO_FTP");<br />

TME.AppendChild(Move);<br />

XmlElement AAM = request.CreateElement("ALL_AGGREGATE_METRICS");<br />

TME.AppendChild(AAM);<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending Tracking Metric Export Request");<br />

//submit the request, and get the response<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! TrackingMetricExport request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

}<br />

}<br />

public string RawRecipientDataExport()<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement RRD = request.CreateElement("RawRecipientDataExport");<br />

request.GetElementsByTagName("Body")[0].AppendChild(RRD);<br />

XmlElement LIST_ID = request.CreateElement("LIST_ID");<br />

LIST_ID.InnerText = MainListId;<br />

RRD.AppendChild(LIST_ID);<br />

XmlElement Move = request.CreateElement("MOVE_TO_FTP");<br />

Side 44


RRD.AppendChild(Move);<br />

XmlElement AET = request.CreateElement("ALL_EVENT_TYPES");<br />

RRD.AppendChild(AET);<br />

XmlElement Columns = request.CreateElement("COLUMNS");<br />

RRD.AppendChild(Columns);<br />

XmlElement Column1 = request.CreateElement("COLUMN");<br />

Columns.AppendChild(Column1);<br />

XmlElement ConId = request.CreateElement("NAME");<br />

ConId.InnerText = "Contact ID";<br />

Column1.AppendChild(ConId);<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending Raw Recipient Data Export Request");<br />

//submit the request, and get the response<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! RawRecipientDataExport request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

return (null);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

}<br />

}<br />

string filename = response.GetElementsByTagName("FILE_PATH")[0].InnerText;<br />

//get job id, and wait for it to complete<br />

WaitOnJobStatus(response.GetElementsByTagName("JOB_ID")[0].InnerText);<br />

return (filename);<br />

private void SubmitRequest()<br />

{<br />

//build a webrequest<br />

WebRequest wrequest = WebRequest.Create(url + sessionencoding) as HttpWebRequest;<br />

//set request properties<br />

wrequest.ContentType = "text/xml;charset=UTF-8";<br />

wrequest.Method = "POST";<br />

StringWriter sw = new StringWriter();<br />

XmlTextWriter xw = new XmlTextWriter(sw);<br />

//save the xml request to a text writer<br />

request.WriteTo(xw);<br />

System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();<br />

//convert to a byte array (the httputility thing requires a reference to system.web.dll)<br />

byte[] byteData = encoding.GetBytes(HttpUtility.UrlPathEncode(sw.ToString()));<br />

wrequest.ContentLength = byteData.Length;<br />

//add data to the request<br />

using (Stream requestStream = wrequest.GetRequestStream())<br />

{<br />

requestStream.Write(byteData, 0, byteData.Length);<br />

}<br />

//finish the request<br />

wrequest.GetRequestStream().Close();<br />

Side 45


}<br />

//get the response<br />

HttpWebResponse wresponse = (HttpWebResponse)wrequest.GetResponse();<br />

//load the response into the response xmlDoc<br />

response = new XmlDocument();<br />

try<br />

{<br />

response.Load(wresponse.GetResponseStream());<br />

}<br />

catch (Exception)<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR while making a request to the Silverpop API - Response could not be loaded!");<br />

}<br />

private void GetJobStatus()<br />

{<br />

if (sessionid == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Can't get a job status");<br />

}<br />

else<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement GetStatus = request.CreateElement("GetJobStatus");<br />

request.GetElementsByTagName("Body")[0].AppendChild(GetStatus);<br />

XmlElement JOB_ID = request.CreateElement("JOB_ID");<br />

JOB_ID.InnerText = jobid;<br />

GetStatus.AppendChild(JOB_ID);<br />

//submit our request and get the response<br />

SubmitRequest();<br />

//check for a null response<br />

if (response == null)<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Error getting Job Status for Job Id: " + jobid);<br />

}<br />

else if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "FALSE") //check that we can get a status for that job<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get job status for job id: " + jobid + " Fault from Silverpop is: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//set the job status<br />

jobstatus = response.GetElementsByTagName("JOB_STATUS")[0].InnerText;<br />

//make a l<strong>og</strong> of the status<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Job status for jobid " + jobid + " is " + jobstatus);<br />

}<br />

}<br />

}<br />

public string TGetJobStatus(string id)<br />

{<br />

if (sessionid == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Can't get a job status");<br />

return ("ERROR");<br />

}<br />

Side 46


else<br />

{<br />

request = null;<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement GetStatus = request.CreateElement("GetJobStatus");<br />

XmlElement JOB_ID = request.CreateElement("JOB_ID");<br />

JOB_ID.InnerText = id;// (string)Thread.GetData(loc);<br />

GetStatus.AppendChild(request.ImportNode(JOB_ID, true));<br />

request.GetElementsByTagName("Body")[0].AppendChild(request.ImportNode(GetStatus,true));<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Requesting Job Status for Job Id: " + id);<br />

//submit our request and get the response<br />

SubmitRequest();<br />

//check for a null response<br />

if (response == null)<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Error getting Job Status for Job Id: " + id);<br />

}<br />

else if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "FALSE") //check that we can get a status for that job<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get job status for job id: " + id + " Fault from Silverpop is: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//make a l<strong>og</strong> of the status<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Job status for jobid " + id + " is " + response.GetElementsByTagName("JOB_STATUS")[0].InnerText);<br />

//set the job status<br />

return(response.GetElementsByTagName("JOB_STATUS")[0].InnerText);<br />

}<br />

return ("ERROR");<br />

}<br />

}<br />

public void WaitOnJobStatus(string id)<br />

{<br />

bool keepgoing=true;<br />

string status;<br />

}<br />

while (keepgoing)<br />

{<br />

status = TGetJobStatus(id);<br />

if (status == "COMPLETE")<br />

{<br />

keepgoing=false;<br />

}<br />

if (status == "WAITING" || jobstatus=="RUNNING")<br />

{<br />

//wait 30 seconds before checking again<br />

Thread.Sleep(30000);<br />

}<br />

if (status == "ERROR")<br />

{<br />

//l<strong>og</strong> the error and end the loop<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

keepgoing = false;<br />

}<br />

}<br />

public void DeleteRecipient(Recipient r)<br />

{<br />

Side 47


MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Creating Delete Request for ID:" + r.Id);<br />

//build request<br />

request = BuildSilverpopWrapper();<br />

XmlElement RemoveRecipient = request.CreateElement("RemoveRecipient");<br />

request.GetElementsByTagName("Body")[0].AppendChild(RemoveRecipient);<br />

XmlElement LIST_ID = request.CreateElement("LIST_ID");<br />

LIST_ID.InnerText = MainListId;<br />

RemoveRecipient.AppendChild(request.ImportNode(LIST_ID,true));<br />

XmlElement EMAIL = request.CreateElement("EMAIL");<br />

EMAIL.InnerText = r.Email;<br />

RemoveRecipient.AppendChild(request.ImportNode(EMAIL,true));<br />

XmlElement COLUMN = request.CreateElement("COLUMN");<br />

RemoveRecipient.AppendChild(COLUMN);<br />

//columns in here must include the name and value for any unique identifiers in the list<br />

XmlElement NAME = request.CreateElement("NAME");<br />

NAME.InnerText = "NEK";<br />

COLUMN.AppendChild(NAME);<br />

XmlElement VALUE = request.CreateElement("VALUE");<br />

VALUE.InnerText = r.Id;<br />

COLUMN.AppendChild(VALUE);<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! RemoveRecipient request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

}<br />

}<br />

}<br />

public string JobId<br />

{<br />

get<br />

{<br />

return(jobid);<br />

}<br />

}<br />

public string JobStatus<br />

{<br />

get<br />

{<br />

return(jobstatus);<br />

}<br />

}<br />

public sealed class SilverpopAPIWatcher<br />

{<br />

private string jid;<br />

private SilverpopAPI api;<br />

public SilverpopAPIWatcher(string id, SilverpopAPI sapi)<br />

Side 48


}<br />

}<br />

}<br />

{<br />

}<br />

jid = id;<br />

api = sapi;<br />

public bool GetJobStatus()<br />

{<br />

bool keepgoing = true;<br />

string status;<br />

bool errors = false;<br />

}<br />

while (keepgoing)<br />

{<br />

status = api.TGetJobStatus(jid);<br />

if (status == "COMPLETE")<br />

{<br />

keepgoing = false;<br />

}<br />

if (status == "WAITING" || status == "RUNNING")<br />

{<br />

//wait 30 seconds before checking again<br />

Thread.Sleep(30000);<br />

}<br />

if (status == "ERROR" || status == "CANCELED")<br />

{<br />

//l<strong>og</strong> the error and end the loop<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Error checking job status");<br />

keepgoing = false;<br />

errors = true;<br />

}<br />

}<br />

return (errors);<br />

public struct Recipient<br />

{<br />

private string id;<br />

private string email;<br />

public string Id<br />

{<br />

get<br />

{<br />

return id;<br />

}<br />

set<br />

{<br />

id = value;<br />

}<br />

}<br />

public string Email<br />

{<br />

get<br />

{<br />

return email;<br />

}<br />

set<br />

{<br />

email = value;<br />

}<br />

}<br />

Side 49


SilverpopFTP.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.Web;<br />

using System.Net;<br />

using System.IO;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public sealed class SilverpopFTP<br />

{<br />

private FtpWebResponse response;<br />

private FtpWebRequest request;<br />

private string username;<br />

private string ftpurl;<br />

private string ftppassword;<br />

public SilverpopFTP()<br />

{<br />

username = System.Configuration.ConfigurationManager.AppSettings["FtpUsername"].ToString();<br />

ftpurl = System.Configuration.ConfigurationManager.AppSettings["FtpUrl"].ToString();<br />

ftppassword = System.Configuration.ConfigurationManager.AppSettings["FtpPassword"].ToString();<br />

}<br />

public void UploadList(string filename)<br />

{<br />

try<br />

{<br />

//l<strong>og</strong> the upload<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Uploading file: " + filename);<br />

//open the file to upload<br />

FileStream fs = File.OpenRead(System.Environment.CurrentDirectory + "\\" + filename);<br />

//generate a byte array from file<br />

byte[] encoded = new byte[fs.Length];<br />

fs.Read(encoded, 0, (int)fs.Length);<br />

fs.Close();<br />

//convert from Unicode to UTF-8<br />

//byte[] encoded = Encoding.Convert(Encoding.GetEncoding("Unicode"), Encoding.GetEncoding("UTF-8"), unencoded, 0,<br />

unencoded.Length);<br />

//set the ftp request properties<br />

request = (FtpWebRequest)FtpWebRequest.Create(ftpurl + filename);<br />

request.ContentLength = encoded.Length;<br />

request.Credentials = new NetworkCredential(username, ftppassword);<br />

request.KeepAlive = false;<br />

//request.EnableSsl = true;<br />

request.Method = WebRequestMethods.Ftp.UploadFile;<br />

//upload isn't supported through http proxy, so request.proxy must be null<br />

request.Proxy = null;<br />

//write the file to the request stream<br />

Stream requeststream = request.GetRequestStream();<br />

requeststream.Write(encoded, 0, encoded.Length);<br />

requeststream.Close();<br />

//get the response<br />

response = (FtpWebResponse)request.GetResponse();<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.StatusDescription);<br />

}<br />

catch(WebException ex)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("A Web Exception Occured: " + ex.ToString());<br />

throw ex;<br />

Side 50


}<br />

}<br />

}<br />

}<br />

public void DownloadReport(string fileToGet, string fileToMake)<br />

{<br />

FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpurl + fileToGet);<br />

request.Method = WebRequestMethods.Ftp.DownloadFile;<br />

}<br />

request.Credentials = new NetworkCredential(username, ftppassword);<br />

FtpWebResponse response = (FtpWebResponse)request.GetResponse();<br />

Stream responseStream = response.GetResponseStream();<br />

StreamReader reader = new StreamReader(responseStream);<br />

FileStream fs = new FileStream(fileToMake, FileMode.Create, FileAccess.Write, FileShare.None);<br />

int length = 256;<br />

Byte[] buffer = new Byte[length];<br />

int bytesRead = responseStream.Read(buffer, 0, length);<br />

while (bytesRead > 0)<br />

{<br />

fs.Write(buffer, 0, bytesRead);<br />

bytesRead = responseStream.Read(buffer, 0, length);<br />

}<br />

responseStream.Close();<br />

fs.Close();<br />

/*StreamWriter sw = new StreamWriter(fs);<br />

byte[] buffer = new byte[responseStream.Length];<br />

sw.Write(responseStream.Read(buffer, 0, responseStream.Length));<br />

sw.Flush();<br />

sw.Close();<br />

fs.Flush();<br />

fs.Close();*/<br />

Side 51


TransactionalDatabase.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.Data;<br />

using System.Data.SqlClient;<br />

using System.Data.Common;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public sealed class TransactionalDatabase<br />

{<br />

private SqlConnection conn;<br />

private string[] sql;<br />

public TransactionalDatabase()<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Creating Database Connection");<br />

conn = new SqlConnection(System.Configuration.ConfigurationManager.AppSettings["FRConnection"].ToString());<br />

}<br />

public string[] Sql<br />

{<br />

get<br />

{<br />

return (sql);<br />

}<br />

set<br />

{<br />

sql = value;<br />

}<br />

}<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("SQL Commands set to:");<br />

foreach (string s in sql)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("SQL Item: " + s);<br />

}<br />

public void RunCommands()<br />

{<br />

//check for any commands to run<br />

if (sql.Length < 1)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("RunCommands() called with no SQL to run!");<br />

return;<br />

}<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("RunCommands() - Opening DB Connection and beginning transaction");<br />

//open the db connection, and create a transaction<br />

conn.Open();<br />

SqlTransaction trans = conn.BeginTransaction();<br />

try<br />

{<br />

//create the command<br />

SqlCommand comm = new SqlCommand();<br />

//set command properties<br />

comm.Connection = conn;<br />

comm.Transaction = trans;<br />

//loop through sql commands and execute<br />

foreach (string s in sql)<br />

{<br />

comm.CommandText = s;<br />

comm.ExecuteNonQuery();<br />

}<br />

Side 52


}<br />

}<br />

}<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Committing Transaction!");<br />

//commit the transaction<br />

trans.Commit();<br />

}<br />

catch<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Transaction failed! Rolling back...");<br />

//roll back the transaction<br />

trans.Rollback();<br />

}<br />

finally<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Closing DB Connection");<br />

//close the connection<br />

conn.Close();<br />

}<br />

public DataSet GetData()<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Retrieving data from database");<br />

}<br />

//open the connection<br />

conn.Open();<br />

//create an sql data adapter<br />

using (SqlDataAdapter sda = new SqlDataAdapter(sql[0], conn))<br />

{<br />

//create the dataset<br />

DataSet ds = new DataSet();<br />

}<br />

//fill the dataset<br />

sda.Fill(ds);<br />

//close the connection<br />

conn.Close();<br />

//and return<br />

return (ds);<br />

Side 53


Pr<strong>og</strong>ram.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using Ionic.Zip;<br />

using System.IO;<br />

using System.Data;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

class Pr<strong>og</strong>ram<br />

{<br />

static void Main(string[] args)<br />

{<br />

SilverpopAPI api = new SilverpopAPI();<br />

SilverpopFTP ftp = new SilverpopFTP();<br />

TransactionalDatabase db = new TransactionalDatabase();<br />

string[] sql = new string[1];<br />

if (api.ApiL<strong>og</strong>in() == true)<br />

{<br />

Console.WriteLine("Successfully l<strong>og</strong>ged into API");<br />

Console.Write("Running export and waiting for it to complete...");<br />

string filename = api.RawRecipientDataExport();<br />

Console.WriteLine("Completed");<br />

Console.Write("Downloading report...");<br />

ftp.DownloadReport(filename, System.Environment.CurrentDirectory + "\\" + filename);<br />

Console.WriteLine("Done");<br />

Console.WriteLine("Extracting files from zip");<br />

using(ZipFile zip = ZipFile.Read(System.Environment.CurrentDirectory + "\\" + filename))<br />

{<br />

zip.ExtractAll(System.Environment.CurrentDirectory);<br />

}<br />

Console.WriteLine("done");<br />

// Deleting Zip file<br />

File.Delete(System.Environment.CurrentDirectory + "\\" + filename);<br />

char[] ziptrim = { 'z', 'i', 'p' };<br />

filename = filename.TrimEnd(ziptrim);<br />

filename = filename + "csv";<br />

try<br />

{<br />

using (FileStream csvfile = new FileStream(System.Environment.CurrentDirectory + "\\" + filename,FileMode.Open,FileAccess.Read))<br />

{<br />

using (TextReader csvread = new StreamReader(csvfile))<br />

{<br />

DataTable CsvData = CsvParser.Parse(csvread, true);<br />

int i = 0;<br />

byte[] TableId = null;<br />

try<br />

{<br />

sql[0] = "SELECT (MAX(Silverpop_In_Id) + 1) AS nextId FROM Silverpop_In";<br />

db.Sql = sql;<br />

Console.WriteLine("Getting Data From Silverpop In");<br />

using (DataSet ds = db.GetData())<br />

{<br />

if (ds != null)<br />

{<br />

DataRow dr = ds.Tables[0].Rows[0];<br />

Side 54


}<br />

if ("" == dr[0].ToString())<br />

TableId = ByteConvertor.ConvertToFossBytes("1");<br />

else<br />

TableId = ByteConvertor.ConvertToFossBytes(dr[0].ToString());<br />

}<br />

else<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get record from Silverpop_In");<br />

ErrorMailGenerator.SendErrorMail("Could not get record from Silverpop_In");<br />

Environment.Exit(0);<br />

}<br />

}<br />

catch (Exception ex)<br />

{<br />

Console.WriteLine("Failing at Getting Data");<br />

throw new Exception(ex.Message);<br />

}<br />

if (CsvData.Rows.Count == 0)<br />

Console.WriteLine("Empty dataset");<br />

foreach (DataRow row in CsvData.Rows) // Loop over the rows.<br />

{<br />

}<br />

Boolean HardBounce = false;<br />

Boolean SoftBounce = false;<br />

Boolean Opened = false;<br />

Boolean Clicked = false;<br />

Boolean OptIn = false;<br />

Boolean OptOut = false;<br />

switch (row["Event Type"].ToString())<br />

{<br />

case "Open" :<br />

Opened = true;<br />

break;<br />

case "Click Through" :<br />

Clicked = true;<br />

break;<br />

case "Soft Bounce":<br />

SoftBounce = true;<br />

break;<br />

case "Hard Bounce":<br />

HardBounce = true;<br />

break;<br />

case "Opt In":<br />

OptIn = true;<br />

break;<br />

case "Opt Out":<br />

OptOut = true;<br />

break;<br />

}<br />

sql[i] = @"INSERT INTO [Silverpop_In]<br />

([Silverpop_In_Id], [Rn_Create_Date], [Contact_Id], [Email], [Email_Opt_In], [Email_Opt_Out],<br />

[Email_Bounced_Hard], [Email_Bounced_Soft], [Email_Bounced_Date], [Email_Clicked], [Email_Clicked_Date],<br />

[Email_Clicked_Link], [Mailing_Description], [Email_Opened], [Email_Opened_Date])<br />

VALUES<br />

(" + TableId + ", " + DateTime.Now.ToString() + ", " + row["Contact Id"] + ", " + row["Email"] + ", " + OptIn<br />

+ ", " + OptOut + ", " + HardBounce + ", " + SoftBounce +", " + row["Event Timestamp"] + ", " + Clicked<br />

+ ", " + row["Event Timestamp"] + ", " + row["URL"] + ", " + row["Mailing Id"] + ", " + Opened + ", " +<br />

row["Event Timestamp"];<br />

i++;<br />

Side 55


}<br />

}<br />

}<br />

db.Sql = sql;<br />

}<br />

}<br />

}<br />

catch (IOException)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("There was an error when trying to read from csv file");<br />

Environment.Exit(0);<br />

}<br />

}<br />

else<br />

{<br />

Console.WriteLine("Could not l<strong>og</strong> in to API");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not l<strong>og</strong> in to API");<br />

}<br />

Console.WriteLine("Press ENTER to exit...");<br />

Console.ReadLine();<br />

Side 56


ConsoleApplication1 C#<br />

App.config<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

Side 57


ByteConvertor.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public static class ByteConvertor<br />

{<br />

/// <br />

/// Converts an decimal id into an bytearray id<br />

/// <br />

/// The decimal representation of the id<br />

/// The Bytearray representation of the id.<br />

public static byte[] ConvertToFossBytes(string id)<br />

{<br />

byte[] converted = new byte[8];<br />

string binstring = "";<br />

}<br />

if (id.Contains("-"))<br />

{<br />

string[] num = id.Split('-');<br />

binstring = DecimalToBase(Convert.ToInt32(num[0]), 2).PadLeft(24, '0');<br />

binstring += DecimalToBase(Convert.ToInt32(num[1]), 2).PadLeft(40, '0');<br />

}<br />

else<br />

{<br />

binstring = DecimalToBase(Convert.ToInt32(id), 2).PadLeft(64, '0');<br />

}<br />

for (int i = 0; i < converted.Length; i++)<br />

{<br />

converted[i] = (byte)BinToDecimal(binstring.Substring(i * 8, 8));<br />

}<br />

return converted;<br />

/// <br />

/// Converts a bytearray representation of an id into a decimal representation of the id<br />

/// <br />

/// bytearray representation of the id<br />

/// the decimal representation of the id<br />

public static string ConvertFossBytes(byte[] workid)<br />

{<br />

string[] BinStringArr = new string[workid.Length];<br />

for (int i = 0; i < workid.Length; i++)<br />

{<br />

BinStringArr[i] = DecimalToBase(workid[i], 2).PadLeft(8, '0');<br />

}<br />

string Head = "";<br />

string Tail = "";<br />

Head = BinStringArr[0];<br />

Head += BinStringArr[1];<br />

Head += BinStringArr[2];<br />

Tail = BinStringArr[3];<br />

Tail += BinStringArr[4];<br />

Tail += BinStringArr[5];<br />

Tail += BinStringArr[6];<br />

Tail += BinStringArr[7];<br />

string HeadOut = BinToDecimal(Head).ToString();<br />

string TailOut = BinToDecimal(Tail).ToString();<br />

Side 58


}<br />

}<br />

}<br />

if (HeadOut == "0")<br />

return TailOut;<br />

else<br />

return HeadOut + "-" + TailOut;<br />

/// <br />

/// Converts a decimal representation of a number into the "base" representation of the number<br />

/// <br />

/// The decimal number<br />

/// The base that the the decimal should be converted into<br />

/// The converted number<br />

public static string DecimalToBase(int iDec, int numbase)<br />

{<br />

string strBin = "";<br />

int[] result = new int[32];<br />

int MaxBit = 32;<br />

for (; iDec > 0; iDec /= numbase)<br />

{<br />

int rem = iDec % numbase;<br />

result[--MaxBit] = rem;<br />

}<br />

for (int i = 0; i < result.Length; i++)<br />

{<br />

strBin += result.GetValue(i);<br />

}<br />

strBin = strBin.TrimStart(new char[] { '0' });<br />

return strBin;<br />

}<br />

/// <br />

/// Converts from binary to decimal<br />

/// <br />

/// binary representation of the number<br />

/// the decimal representation of the number <br />

public static int BinToDecimal(string sBase)<br />

{<br />

int dec = 0;<br />

int bit = 0;<br />

for (int i = sBase.Length - 1; i >= 0; i--)<br />

{<br />

if ("1" == sBase[i].ToString())<br />

{<br />

dec += (int)Math.Pow(2, bit);<br />

}<br />

bit++;<br />

}<br />

return dec;<br />

}<br />

Side 59


CsvFileGenerator.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.IO;<br />

using System.Data;<br />

using System.Data.SqlClient;<br />

using System.Data.Common;<br />

using System.Text.RegularExpressions;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public sealed class CsvFileGenerator<br />

{<br />

private string filename;<br />

public string FileName<br />

{<br />

get<br />

{<br />

return (filename);<br />

}<br />

set<br />

{<br />

filename = value;<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Filename changed to: " + filename);<br />

}<br />

}<br />

public void CreateMainFile(DataRow dr)<br />

{<br />

StreamWriter sw = File.AppendText(filename);<br />

try<br />

{<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Contact_Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Lead_Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

sw.Write(FormatForCSV(dr["First_Name"].ToString()));<br />

sw.Write(FormatForCSV(dr["Last_Name"].ToString()));<br />

sw.Write(FormatForCSV(dr["Job_Category"].ToString()));<br />

sw.Write(FormatForCSV(dr["Country"].ToString()));<br />

sw.Write(FormatForCSV(dr["E_Mail"].ToString()));<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Company_Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

sw.Write(FormatForCSV(dr["Company_Name"].ToString()));<br />

sw.Write(FormatForCSV(dr["RSM_First_Name"].ToString()));<br />

sw.Write(FormatForCSV(dr["RSM_Last_Name"].ToString()));<br />

Side 60


}<br />

sw.Write(FormatForCSV(dr["RSM_Job_Title"].ToString()));<br />

sw.Write(FormatForCSV(dr["RSM_Company_Name"].ToString()));<br />

sw.Write(FormatForCSV(dr["RSM_E_Mail"].ToString()));<br />

sw.Write(FormatForCSV(dr["RSM_Phone"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Analytical_Labs"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Beer"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Biofuel"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Chemicals"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Confectionery"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Dressings"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Edible_Oils"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Feed_and_Forage"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Flour"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Grain"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Meat"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Microbiol<strong>og</strong>y"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Milk_and_Dairy"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Pet_Food"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Pharmaceuticals"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Sugar"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Water_and_Soil"].ToString()));<br />

sw.Write(FormatForCSV(dr["BA_Wine"].ToString()));<br />

string part1 = null;<br />

string part2 = null;<br />

try<br />

{<br />

part1 = ByteConvertor.ConvertFossBytes((byte[])dr["Contact_Id"]);<br />

}<br />

catch<br />

{<br />

part2 = ByteConvertor.ConvertFossBytes((byte[])dr["Lead_Id"]);<br />

}<br />

finally<br />

{<br />

sw.WriteLine(FormatForCSV(part1 + part2,true));<br />

}<br />

}<br />

finally<br />

{<br />

sw.Close();<br />

}<br />

public void CreateProductFile(DataRow dr)<br />

{<br />

StreamWriter sw = File.AppendText(filename);<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Registration_Id"])));<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Company_Id"])));<br />

sw.Write(FormatForCSV(dr["Product_Name"].ToString()));<br />

sw.WriteLine(FormatForCSV(dr["Product_Group_Name"].ToString(), true));<br />

}<br />

finally<br />

{<br />

sw.Close();<br />

}<br />

}<br />

public void CreateBAFile(DataRow dr)<br />

{<br />

StreamWriter sw = File.AppendText(filename);<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Contact_Customer_Segment_Id"])));<br />

Side 61


}<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Contact_Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Lead_Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

sw.WriteLine(FormatForCSV(dr["Segment"].ToString(), true));<br />

}<br />

finally<br />

{<br />

sw.Close();<br />

}<br />

public void CreatePrefCommFile(DataRow dr)<br />

{<br />

StreamWriter sw = File.AppendText(filename);<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Preferred_Com_Methods_Id"])));<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Contact_Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

try<br />

{<br />

sw.Write(FormatForCSV(ByteConvertor.ConvertFossBytes((byte[])dr["Lead__Id"])));<br />

}<br />

catch<br />

{<br />

sw.Write(FormatForCSV(""));<br />

}<br />

}<br />

sw.WriteLine(FormatForCSV(dr["Type"].ToString(), true));<br />

}<br />

finally<br />

{<br />

sw.Close();<br />

}<br />

public void DeleteFile()<br />

{<br />

try<br />

{<br />

File.Delete(filename);<br />

}<br />

catch (Exception ex)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Unable To Delete File! " + ex.ToString());<br />

}<br />

Side 62


}<br />

}<br />

}<br />

private string FormatForCSV(string s, bool last)<br />

{<br />

//escape any speech marks in the string<br />

string pattern = "\"";<br />

}<br />

Regex regex = new Regex(pattern);<br />

Match match = regex.Match(s);<br />

s = regex.Replace(s, "\"\"");<br />

//if this is the last item in a row, omit the trailing comma<br />

if (last)<br />

{<br />

return ("\"" + s + "\"");<br />

}<br />

else<br />

{<br />

return ("\"" + s + "\",");<br />

}<br />

private string FormatForCSV(string s)<br />

{<br />

return(FormatForCSV(s, false));<br />

}<br />

Side 63


SilverpopAPI.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.Xml;<br />

using System.Configuration;<br />

using System.Web;<br />

using System.Net;<br />

using System.IO;<br />

using System.Threading;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public sealed class SilverpopAPI<br />

{<br />

private string sessionid;<br />

private string sessionencoding;<br />

private XmlDocument request;<br />

private XmlDocument response;<br />

private string jobstatus;<br />

private string url;<br />

private string username;<br />

private string password;<br />

private string jobid;<br />

private static string MainListId = System.Configuration.ConfigurationManager.AppSettings["MainListId"].ToString();<br />

public SilverpopAPI()<br />

{<br />

url = System.Configuration.ConfigurationManager.AppSettings["ApiUrl"].ToString();<br />

username = System.Configuration.ConfigurationManager.AppSettings["Username"].ToString();<br />

password = System.Configuration.ConfigurationManager.AppSettings["Password"].ToString();<br />

}<br />

request = new XmlDocument();<br />

response = new XmlDocument();<br />

public bool ApiL<strong>og</strong>in()<br />

{<br />

if (sessionid == null)<br />

{<br />

//create xml document<br />

request = BuildSilverpopWrapper();<br />

XmlElement L<strong>og</strong>in = request.CreateElement("L<strong>og</strong>in");<br />

request.GetElementsByTagName("Body")[0].AppendChild(L<strong>og</strong>in);<br />

//add username and password from app.config<br />

XmlElement usernameXML = request.CreateElement("USERNAME");<br />

usernameXML.InnerText = username;<br />

L<strong>og</strong>in.AppendChild(usernameXML);<br />

XmlElement passwordXML = request.CreateElement("PASSWORD");<br />

passwordXML.InnerText = password;<br />

L<strong>og</strong>in.AppendChild(passwordXML);<br />

//l<strong>og</strong>ging in<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("L<strong>og</strong>ging Into Silverpop API...");<br />

SubmitRequest();<br />

if (response == null) return (false);<br />

//parse the response to check if l<strong>og</strong>ged in, and get the sessionid<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "true")<br />

{<br />

sessionid = response.GetElementsByTagName("SESSIONID")[0].InnerText;<br />

sessionencoding = response.GetElementsByTagName("SESSION_ENCODING")[0].InnerText;<br />

Side 64


}<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Successfully l<strong>og</strong>ged in with sessionid: " + sessionid);<br />

return (true);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Failed to l<strong>og</strong>in to API :: " + response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

return (false);<br />

}<br />

else<br />

{<br />

//already l<strong>og</strong>ged in<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Already l<strong>og</strong>ged into Silverpop API");<br />

return (true);<br />

}<br />

public void ApiL<strong>og</strong>out()<br />

{<br />

if (sessionid == null)<br />

{<br />

//not l<strong>og</strong>ged in it seems<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Already l<strong>og</strong>ged out of API");<br />

//ensure variables are reset<br />

sessionencoding = null;<br />

}<br />

else<br />

{<br />

//create l<strong>og</strong>out request<br />

request = BuildSilverpopWrapper();<br />

}<br />

}<br />

XmlElement l<strong>og</strong>out = request.CreateElement("L<strong>og</strong>out");<br />

request.GetElementsByTagName("Body")[0].AppendChild(l<strong>og</strong>out);<br />

//l<strong>og</strong>ging out<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("L<strong>og</strong>ging out of API");<br />

//submit request and get response<br />

SubmitRequest();<br />

if (response == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Null response when attempting to l<strong>og</strong> out");<br />

}<br />

sessionencoding = null;<br />

sessionid = null;<br />

private XmlDocument BuildSilverpopWrapper()<br />

{<br />

XmlDocument xml = new XmlDocument();<br />

XmlDeclaration xmlDec = xml.CreateXmlDeclaration("1.0", "utf-8", null);<br />

XmlElement envelope = xml.CreateElement("Envelope");<br />

xml.InsertBefore(xmlDec, xml.DocumentElement);<br />

xml.AppendChild(envelope);<br />

XmlElement body = xml.CreateElement("Body");<br />

envelope.AppendChild(body);<br />

return (xml);<br />

Side 65


}<br />

public void ListImport(string list)<br />

{<br />

if (sessionid == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Attempted to make an API Request without being l<strong>og</strong>ged in!");<br />

}<br />

else<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement ImportList_ = request.CreateElement("ImportList");<br />

request.GetElementsByTagName("Body")[0].AppendChild(ImportList_);<br />

XmlElement MAP_FILE = request.CreateElement("MAP_FILE");<br />

MAP_FILE.InnerText = list + ".xml";<br />

ImportList_.AppendChild(MAP_FILE);<br />

XmlElement SOURCE_FILE = request.CreateElement("SOURCE_FILE");<br />

SOURCE_FILE.InnerText = list + ".csv";<br />

ImportList_.AppendChild(SOURCE_FILE);<br />

//l<strong>og</strong> the request<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending ListImport Request to Silverpop API for list: " + list);<br />

//make the request and get response<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! ListImport request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

jobid = response.GetElementsByTagName("JOB_ID")[0].InnerText;<br />

}<br />

}<br />

}<br />

public void TableImport(string list)<br />

{<br />

if (sessionid == null)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Attempted to make an API Request without being l<strong>og</strong>ged in!");<br />

}<br />

else<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement ImportTable_ = request.CreateElement("ImportTable");<br />

request.GetElementsByTagName("Body")[0].AppendChild(ImportTable_);<br />

XmlElement MAP_FILE = request.CreateElement("MAP_FILE");<br />

MAP_FILE.InnerText = list + ".xml";<br />

ImportTable_.AppendChild(MAP_FILE);<br />

XmlElement SOURCE_FILE = request.CreateElement("SOURCE_FILE");<br />

SOURCE_FILE.InnerText = list + ".csv";<br />

ImportTable_.AppendChild(SOURCE_FILE);<br />

//l<strong>og</strong> the request<br />

Side 66


MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Sending TableImport Request to Silverpop API for list: " + list);<br />

//make the request and get response<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! TableImport request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

jobid = response.GetElementsByTagName("JOB_ID")[0].InnerText;<br />

}<br />

}<br />

}<br />

private void SubmitRequest()<br />

{<br />

//build a webrequest<br />

WebRequest wrequest = WebRequest.Create(url + sessionencoding) as HttpWebRequest;<br />

}<br />

//set request properties<br />

wrequest.ContentType = "text/xml;charset=UTF-8";<br />

wrequest.Method = "POST";<br />

StringWriter sw = new StringWriter();<br />

XmlTextWriter xw = new XmlTextWriter(sw);<br />

//save the xml request to a text writer<br />

request.WriteTo(xw);<br />

System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();<br />

//convert to a byte array<br />

byte[] byteData = encoding.GetBytes(HttpUtility.UrlPathEncode(sw.ToString()));<br />

wrequest.ContentLength = byteData.Length;<br />

//add data to the request<br />

using (Stream requestStream = wrequest.GetRequestStream())<br />

{<br />

requestStream.Write(byteData, 0, byteData.Length);<br />

}<br />

//finish the request<br />

wrequest.GetRequestStream().Close();<br />

//get the response<br />

HttpWebResponse wresponse = (HttpWebResponse)wrequest.GetResponse();<br />

//load the response into the response xmlDoc<br />

response = new XmlDocument();<br />

try<br />

{<br />

response.Load(wresponse.GetResponseStream());<br />

}<br />

catch (Exception)<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR while making a request to the Silverpop API - Response could not be loaded!");<br />

}<br />

private void GetJobStatus()<br />

Side 67


{<br />

if (sessionid == null)<br />

{<br />

//not l<strong>og</strong>ged in<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Can't get a job status, not l<strong>og</strong>ged in");<br />

}<br />

else<br />

{<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement GetStatus = request.CreateElement("GetJobStatus");<br />

request.GetElementsByTagName("Body")[0].AppendChild(GetStatus);<br />

XmlElement JOB_ID = request.CreateElement("JOB_ID");<br />

JOB_ID.InnerText = jobid;<br />

GetStatus.AppendChild(JOB_ID);<br />

//submit our request and get the response<br />

SubmitRequest();<br />

//check for a null response<br />

if (response == null)<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Error getting Job Status for Job Id: " + jobid);<br />

}<br />

else if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "FALSE") //check that we can get a status for that job<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get job status for job id: " + jobid + " Fault from Silverpop is: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//set the job status<br />

jobstatus = response.GetElementsByTagName("JOB_STATUS")[0].InnerText;<br />

//make a l<strong>og</strong> of the status<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Job status for jobid " + jobid + " is " + jobstatus);<br />

}<br />

}<br />

}<br />

public string TGetJobStatus(string id)<br />

{<br />

if (sessionid == null)<br />

{<br />

//not l<strong>og</strong>ged in<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Can't get a job status, not l<strong>og</strong>ged in");<br />

return ("ERROR");<br />

}<br />

else<br />

{<br />

request = null;<br />

//build our request<br />

request = BuildSilverpopWrapper();<br />

XmlElement GetStatus = request.CreateElement("GetJobStatus");<br />

XmlElement JOB_ID = request.CreateElement("JOB_ID");<br />

JOB_ID.InnerText = id;// (string)Thread.GetData(loc);<br />

GetStatus.AppendChild(request.ImportNode(JOB_ID, true));<br />

request.GetElementsByTagName("Body")[0].AppendChild(request.ImportNode(GetStatus,true));<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Requesting Job Status for Job Id: " + id);<br />

//submit our request and get the response<br />

SubmitRequest();<br />

Side 68


check for a null response<br />

if (response == null)<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Error getting Job Status for Job Id: " + id);<br />

}<br />

else if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "FALSE") //check that we can get a status for that job<br />

{<br />

//l<strong>og</strong> an error<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get job status for job id: " + id + " Fault from Silverpop is: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//make a l<strong>og</strong> of the status<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Job status for jobid " + id + " is " + response.GetElementsByTagName("JOB_STATUS")[0].InnerText);<br />

//set the job status<br />

return(response.GetElementsByTagName("JOB_STATUS")[0].InnerText);<br />

}<br />

return ("ERROR");<br />

}<br />

}<br />

public void WaitOnJobStatus(string id)<br />

{<br />

bool keepgoing=true;<br />

string status;<br />

}<br />

//LocalDataStoreSlot localSlot = Thread.AllocateDataSlot();<br />

//Thread.SetData(localSlot, id);<br />

while (keepgoing)<br />

{<br />

status = TGetJobStatus(id);<br />

if (status == "COMPLETE")<br />

{<br />

keepgoing=false;<br />

}<br />

if (status == "WAITING" || jobstatus=="RUNNING")<br />

{<br />

//wait 30 seconds before checking again<br />

Thread.Sleep(30000);<br />

}<br />

if (status == "ERROR")<br />

{<br />

//l<strong>og</strong> the error and end the loop<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

keepgoing = false;<br />

}<br />

}<br />

public void DeleteRecipient(Recipient r)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Creating Delete Request for ID:" + r.Id);<br />

//build request<br />

request = BuildSilverpopWrapper();<br />

XmlElement RemoveRecipient = request.CreateElement("RemoveRecipient");<br />

request.GetElementsByTagName("Body")[0].AppendChild(RemoveRecipient);<br />

XmlElement LIST_ID = request.CreateElement("LIST_ID");<br />

LIST_ID.InnerText = MainListId;<br />

RemoveRecipient.AppendChild(request.ImportNode(LIST_ID,true));<br />

XmlElement EMAIL = request.CreateElement("EMAIL");<br />

EMAIL.InnerText = r.Email;<br />

Side 69


RemoveRecipient.AppendChild(request.ImportNode(EMAIL,true));<br />

XmlElement COLUMN = request.CreateElement("COLUMN");<br />

RemoveRecipient.AppendChild(COLUMN);<br />

//columns in here must include the name and value for any unique identifiers in the list<br />

XmlElement NAME = request.CreateElement("NAME");<br />

NAME.InnerText = "NEK";<br />

COLUMN.AppendChild(NAME);<br />

XmlElement VALUE = request.CreateElement("VALUE");<br />

VALUE.InnerText = r.Id;<br />

COLUMN.AppendChild(VALUE);<br />

SubmitRequest();<br />

//if the request was not successful record an error<br />

if (response.GetElementsByTagName("SUCCESS")[0].InnerText == "false")<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("ERROR! RemoveRecipient request returned the following fault: " +<br />

response.GetElementsByTagName("FaultString")[0].InnerText);<br />

}<br />

else<br />

{<br />

//l<strong>og</strong> success<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.FirstChild.InnerText.ToString());<br />

}<br />

}<br />

}<br />

public string JobId<br />

{<br />

get<br />

{<br />

return(jobid);<br />

}<br />

}<br />

public string JobStatus<br />

{<br />

get<br />

{<br />

return(jobstatus);<br />

}<br />

}<br />

public sealed class SilverpopAPIWatcher<br />

{<br />

private string jid;<br />

private SilverpopAPI api;<br />

public SilverpopAPIWatcher(string id, SilverpopAPI sapi)<br />

{<br />

jid = id;<br />

api = sapi;<br />

}<br />

public bool GetJobStatus()<br />

{<br />

bool keepgoing = true;<br />

string status;<br />

bool errors = false;<br />

while (keepgoing)<br />

{<br />

status = api.TGetJobStatus(jid);<br />

Side 70


}<br />

}<br />

}<br />

}<br />

}<br />

if (status == "COMPLETE")<br />

{<br />

keepgoing = false;<br />

}<br />

if (status == "WAITING" || status == "RUNNING")<br />

{<br />

//wait 30 seconds before checking again<br />

Thread.Sleep(30000);<br />

}<br />

if (status == "ERROR" || status == "CANCELED")<br />

{<br />

//l<strong>og</strong> the error and end the loop<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Error checking job status");<br />

keepgoing = false;<br />

errors = true;<br />

}<br />

return (errors);<br />

public struct Recipient<br />

{<br />

private string id;<br />

private string email;<br />

public string Id<br />

{<br />

get<br />

{<br />

return id;<br />

}<br />

set<br />

{<br />

id = value;<br />

}<br />

}<br />

public string Email<br />

{<br />

get<br />

{<br />

return email;<br />

}<br />

set<br />

{<br />

email = value;<br />

}<br />

}<br />

Side 71


SilverpopFTP.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using System.Web;<br />

using System.Net;<br />

using System.IO;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

public sealed class SilverpopFTP<br />

{<br />

private FtpWebResponse response;<br />

private FtpWebRequest request;<br />

private string username;<br />

private string ftpurl;<br />

private string ftppassword;<br />

public SilverpopFTP()<br />

{<br />

username = System.Configuration.ConfigurationManager.AppSettings["FtpUsername"].ToString();<br />

ftpurl = System.Configuration.ConfigurationManager.AppSettings["FtpUrl"].ToString();<br />

ftppassword = System.Configuration.ConfigurationManager.AppSettings["FtpPassword"].ToString();<br />

}<br />

public void UploadList(string filename)<br />

{<br />

try<br />

{<br />

//l<strong>og</strong> the upload<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Uploading file: " + filename);<br />

//open the file to upload<br />

FileStream fs = File.OpenRead(System.Environment.CurrentDirectory + "\\" + filename);<br />

//generate a byte array from file<br />

byte[] encoded = new byte[fs.Length];<br />

fs.Read(encoded, 0, (int)fs.Length);<br />

fs.Close();<br />

//convert from Unicode to UTF-8<br />

//byte[] encoded = Encoding.Convert(Encoding.GetEncoding("Unicode"), Encoding.GetEncoding("UTF-8"), unencoded, 0,<br />

unencoded.Length);<br />

//set the ftp request properties<br />

request = (FtpWebRequest)FtpWebRequest.Create(ftpurl + filename);<br />

request.ContentLength = encoded.Length;<br />

request.Credentials = new NetworkCredential(username, ftppassword);<br />

request.KeepAlive = false;<br />

//request.EnableSsl = true;<br />

request.Method = WebRequestMethods.Ftp.UploadFile;<br />

//upload isn't supported through http proxy, so request.proxy must be null<br />

request.Proxy = null;<br />

//write the file to the request stream<br />

Stream requeststream = request.GetRequestStream();<br />

requeststream.Write(encoded, 0, encoded.Length);<br />

requeststream.Close();<br />

//get the response<br />

response = (FtpWebResponse)request.GetResponse();<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry(response.StatusDescription);<br />

}<br />

catch(WebException ex)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("A Web Exception Occured: " + ex.ToString());<br />

throw ex;<br />

Side 72


}<br />

}<br />

}<br />

}<br />

Side 73


Pr<strong>og</strong>ram.cs<br />

using System;<br />

using System.Collections.Generic;<br />

using System.Text;<br />

using FOSS.SilverpopIntegration;<br />

using System.Data;<br />

using System.Threading;<br />

using System.IO;<br />

namespace FOSS.SilverpopIntegration<br />

{<br />

class SilverpopSync<br />

{<br />

static void Main(string[] args)<br />

{<br />

TransactionalDatabase db = new TransactionalDatabase();<br />

CsvFileGenerator csv = new CsvFileGenerator();<br />

SilverpopFTP ftp = new SilverpopFTP();<br />

SilverpopAPI api = new SilverpopAPI();<br />

int NUMBER_OF_FILES = 4;<br />

string[] sql = new string[1];<br />

#region deletions<br />

//run through all delete requests<br />

sql[0] = "SELECT * FROM Silverpop_Out WHERE Deletion = 1";<br />

db.Sql = sql;<br />

Console.WriteLine("Getting Deletion Records");<br />

using (DataSet ds = db.GetData())<br />

{<br />

if (ds != null)<br />

{<br />

if (ds.Tables[0].Rows.Count == 0)<br />

{<br />

Console.WriteLine("There are no deletions");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("There are no deletions");<br />

}<br />

else<br />

{<br />

if (api.ApiL<strong>og</strong>in())<br />

{<br />

Console.WriteLine("Starting Deletions...");<br />

List events = new List();<br />

foreach (DataRow dr in ds.Tables[0].Rows)<br />

{<br />

Recipient r = new Recipient();<br />

string part1 = null;<br />

string part2 = null;<br />

try<br />

{<br />

part1 = ByteConvertor.ConvertFossBytes((byte[])dr["Contact_Id"]);<br />

}<br />

catch<br />

{<br />

part2 = ByteConvertor.ConvertFossBytes((byte[])dr["Lead_Id"]);<br />

}<br />

finally<br />

{<br />

r.Id = part1 + part2;<br />

}<br />

r.Email = dr["E_Mail"].ToString();<br />

ManualResetEvent mre = new ManualResetEvent(false);<br />

ThreadPool.QueueUserWorkItem(<br />

delegate(object o)<br />

Side 74


{<br />

api.DeleteRecipient(r);<br />

((ManualResetEvent)o).Set();<br />

}, mre);<br />

events.Add(mre);<br />

}<br />

Console.Write("Waiting for delete requests to be submitted...");<br />

WaitHandle.WaitAll(events.ToArray());<br />

Console.WriteLine("Done!");<br />

}<br />

else<br />

{<br />

Console.ForegroundColor = ConsoleColor.Red;<br />

Console.WriteLine("ERROR LOGGING INTO API - CHECK USERNAME AND PASSWORD");<br />

//send an error mail<br />

ErrorMailGenerator.SendErrorMail("Couldn't l<strong>og</strong> into the api, check username and password");<br />

Environment.Exit(0);<br />

}<br />

}<br />

}<br />

else<br />

{<br />

Console.WriteLine("Couldn't get deletions data");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Couldn't get any deletions data...");<br />

}<br />

}<br />

#endregion<br />

#region MainList<br />

//create and upload MainList<br />

csv.FileName = System.Environment.CurrentDirectory + "\\MainList.csv";<br />

sql[0] = "SELECT * FROM Silverpop_Out WHERE Deletion=0";<br />

db.Sql = sql;<br />

Console.WriteLine("Getting Data");<br />

using (DataSet ds = db.GetData())<br />

{<br />

if (ds != null)<br />

{<br />

if (ds.Tables[0].Rows.Count == 0)<br />

{<br />

Console.WriteLine("Empty Mainlist Dataset, exiting pr<strong>og</strong>ram");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Empty Mainlist Dataset, exiting pr<strong>og</strong>ram");<br />

Environment.Exit(0);<br />

}<br />

Console.WriteLine("Making file");<br />

foreach (DataRow dr in ds.Tables["Table"].Rows)<br />

{<br />

csv.CreateMainFile(dr);<br />

}<br />

Console.WriteLine("Uploading File");<br />

ftp.UploadList("MainList.csv");<br />

ftp.UploadList("MainList.xml");<br />

csv.DeleteFile();<br />

}<br />

else<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get dataset when attempting to retrieve mainlist");<br />

ErrorMailGenerator.SendErrorMail("Could not get dataset when attempting to retrieve mainlist");<br />

Environment.Exit(0);<br />

}<br />

Side 75


}<br />

#endregion<br />

#region Products<br />

//Create and upload Products<br />

csv.FileName = System.Environment.CurrentDirectory + "\\products.csv";<br />

sql[0] = @"SELECT DISTINCT Registration.Registration_Id, Registration.Company_Id, Registration.Product_Name,<br />

Product_Group.Product_Group_Name<br />

FROM Silverpop_Out, Registration, Product_Group, Product<br />

WHERE Silverpop_Out.Company_Id = Registration.Company_Id<br />

AND Registration.Product_Id = Product.Product_Id<br />

AND Product.Product_Group_Id = Product_Group.Product_Group_Id<br />

AND Registration.Formerly_Installed = 0";<br />

db.Sql = sql;<br />

Console.WriteLine("Getting Data");<br />

using (DataSet ds = db.GetData())<br />

{<br />

if (ds != null)<br />

{<br />

Console.WriteLine("Making file");<br />

foreach (DataRow dr in ds.Tables["Table"].Rows)<br />

{<br />

csv.CreateProductFile(dr);<br />

}<br />

Console.WriteLine("Uploading File");<br />

if (File.Exists(System.Environment.CurrentDirectory + "\\products.csv"))<br />

{<br />

ftp.UploadList("products.csv");<br />

ftp.UploadList("products.xml");<br />

csv.DeleteFile();<br />

}<br />

else<br />

{<br />

Console.WriteLine("Products not uploaded - file does not exist");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Products not uploaded - file does not exist");<br />

}<br />

}<br />

else<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get dataset when attempting to retrieve products");<br />

ErrorMailGenerator.SendErrorMail("Could not get dataset when attempting to retrieve products");<br />

}<br />

}<br />

#endregion<br />

#region Business Areas<br />

//Create and upload BA<br />

csv.FileName = System.Environment.CurrentDirectory + "\\ba.csv";<br />

sql[0] = @"SELECT Contact_Customer_Segment.Contact_Customer_Segment_Id, Silverpop_Out.Contact_Id, Silverpop_Out.Lead_Id,<br />

Contact_Customer_Segment.Segment<br />

FROM Contact_Customer_Segment, Silverpop_Out<br />

WHERE Contact_Customer_Segment.Contact_Id = Silverpop_Out.Contact_Id<br />

OR Contact_Customer_Segment.Lead_Id = Silverpop_Out.Lead_Id";<br />

db.Sql = sql;<br />

Console.WriteLine("Getting Data");<br />

using (DataSet ds = db.GetData())<br />

{<br />

if (ds != null)<br />

{<br />

Console.WriteLine("Making file");<br />

foreach (DataRow dr in ds.Tables["Table"].Rows)<br />

Side 76


{<br />

csv.CreateBAFile(dr);<br />

}<br />

Console.WriteLine("Uploading File");<br />

if (File.Exists(System.Environment.CurrentDirectory + "\\ba.csv"))<br />

{<br />

ftp.UploadList("ba.csv");<br />

ftp.UploadList("ba.xml");<br />

csv.DeleteFile();<br />

}<br />

else<br />

{<br />

Console.WriteLine("BA not uploaded - file does not exist");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("BA not uploaded - file does not exist");<br />

}<br />

}<br />

else<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get dataset when attempting to retrieve business areas");<br />

ErrorMailGenerator.SendErrorMail("Could not get dataset when attempting to retrieve business areas");<br />

}<br />

}<br />

#endregion<br />

#region Pref Comm Methods<br />

csv.FileName = System.Environment.CurrentDirectory + "\\pref_comm.csv";<br />

sql[0] = @"SELECT Preferred_Com_Methods.Preferred_Com_Methods_Id, Preferred_Com_Methods.Contact_Id,<br />

Preferred_Com_Methods.Lead__Id, Preferred_Com_Methods.Type<br />

FROM Preferred_Com_Methods, Silverpop_Out<br />

WHERE Preferred_Com_Methods.Contact_Id = Silverpop_Out.Contact_Id<br />

OR Preferred_Com_Methods.Lead__Id = Silverpop_Out.Lead_Id";<br />

db.Sql = sql;<br />

Console.WriteLine("Getting Data");<br />

using (DataSet ds = db.GetData())<br />

{<br />

if (ds != null)<br />

{<br />

Console.WriteLine("Making file");<br />

foreach (DataRow dr in ds.Tables["Table"].Rows)<br />

{<br />

csv.CreatePrefCommFile(dr);<br />

}<br />

Console.WriteLine("Uploading File");<br />

if (File.Exists(System.Environment.CurrentDirectory + "\\pref_comm.csv"))<br />

{<br />

ftp.UploadList("pref_comm.csv");<br />

ftp.UploadList("pref_comm.xml");<br />

csv.DeleteFile();<br />

}<br />

else<br />

{<br />

Console.WriteLine("pref comms not uploaded - file does not exist");<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("pref comms not uploaded - file does not exist");<br />

}<br />

}<br />

else<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Could not get dataset when attempting to retrieve Preferred Communication Methods");<br />

ErrorMailGenerator.SendErrorMail("Could not get dataset when attempting to retrieve Preferred Communication Methods");<br />

}<br />

}<br />

#endregion<br />

Side 77


String[] jobids = new String[NUMBER_OF_FILES];<br />

if (api.ApiL<strong>og</strong>in())<br />

{<br />

Console.WriteLine("Successfully l<strong>og</strong>ged into <strong>silverpop</strong> api");<br />

#region Start Imports<br />

//start the imports and get job ids<br />

Console.WriteLine("Attempting to import: MainList");<br />

api.ListImport("MainList");<br />

jobids[0] = api.JobId;<br />

Console.WriteLine("Job ID for import is: {0}", api.JobId);<br />

Console.WriteLine("Attempting to import: products");<br />

api.TableImport("products");<br />

jobids[1] = api.JobId;<br />

Console.WriteLine("Job ID for import is: {0}", api.JobId);<br />

Console.WriteLine("Attempting to import: ba");<br />

api.TableImport("ba");<br />

jobids[2] = api.JobId;<br />

Console.WriteLine("Job ID for import is: {0}", api.JobId);<br />

Console.WriteLine("Attempting to import: pref comms");<br />

api.TableImport("pref_comm");<br />

jobids[3] = api.JobId;<br />

Console.WriteLine("Job ID for import is: {0}", api.JobId);<br />

#endregion<br />

//start a threadpool to wait for each import to complete<br />

List events = new List();<br />

bool started = false;<br />

bool errorsEncountered = false;<br />

foreach (string i in jobids)<br />

{<br />

started = false;<br />

}<br />

Console.WriteLine("Item added to threadpool for jobid: " + i);<br />

ManualResetEvent mre = new ManualResetEvent(false);<br />

ThreadPool.QueueUserWorkItem(<br />

delegate(object o)<br />

{<br />

SilverpopAPIWatcher apiw = new SilverpopAPIWatcher(i, api);<br />

started = true;<br />

if (apiw.GetJobStatus() == true) errorsEncountered = true;<br />

((ManualResetEvent)o).Set();<br />

},mre);<br />

events.Add(mre);<br />

while (!started)<br />

{<br />

Console.WriteLine("It's not ready yet");<br />

}<br />

Console.WriteLine("All Items added, waiting for imports to finish...");<br />

WaitHandle.WaitAll(events.ToArray());<br />

if (errorsEncountered)<br />

{<br />

MagicalL<strong>og</strong>File.MakeL<strong>og</strong>Entry("Errors were encountered during API Import Operations, earlier l<strong>og</strong> entries should include more<br />

information");<br />

ErrorMailGenerator.SendErrorMail("Errors were encountered during API Import Operations, earlier l<strong>og</strong> entries should include more<br />

information");<br />

Environment.Exit(0);<br />

}<br />

api.ApiL<strong>og</strong>out();<br />

Console.WriteLine("Emptying Silverpop_Out Table");<br />

Side 78


}<br />

}<br />

}<br />

sql[0] = "TRUNCATE TABLE Silverpop_Out";<br />

db.Sql = sql;<br />

db.RunCommands();<br />

//Console.WriteLine("Press Enter to exit...");<br />

//Console.ReadLine();<br />

}<br />

else<br />

{<br />

Console.ForegroundColor = ConsoleColor.Red;<br />

Console.WriteLine("ERROR LOGGING INTO API - CHECK USERNAME AND PASSWORD");<br />

}<br />

//send an error mail<br />

ErrorMailGenerator.SendErrorMail("Couldn't l<strong>og</strong> into the api, please check the username and password");<br />

Environment.Exit(0);<br />

Side 79


MainList.xml<br />

<br />

<br />

<br />

ADD_AND_UPDATE<br />

6834860<br />

1<br />

0<br />

<br />

<br />

<br />

Contact Id<br />

0<br />

<br />

<br />

Lead Id<br />

0<br />

<br />

<br />

First Name<br />

0<br />

<br />

<br />

Last Name<br />

0<br />

<br />

<br />

Job Category<br />

0<br />

<br />

<br />

Country<br />

0<br />

<br />

<br />

EMAIL<br />

9<br />

true<br />

<br />

<br />

Company Id<br />

0<br />

<br />

<br />

Company Name<br />

0<br />

<br />

<br />

RSM First Name<br />

0<br />

<br />

<br />

RSM Last Name<br />

0<br />

<br />

<br />

RSM Job Title<br />

0<br />

<br />

<br />

RSM Company<br />

0<br />

<br />

<br />

RSM EMail<br />

0<br />

<br />

<br />

Side 80


RSM Phone<br />

0<br />

<br />

<br />

BA Analytical Labs<br />

0<br />

<br />

<br />

BA Beer<br />

0<br />

<br />

<br />

BA Biofuel<br />

0<br />

<br />

<br />

BA Chemicals<br />

0<br />

<br />

<br />

BA Confectionery<br />

0<br />

<br />

<br />

BA Dressings and Condiments<br />

0<br />

<br />

<br />

BA Edible Oils and Fats<br />

0<br />

<br />

<br />

BA Feed and Forage<br />

0<br />

<br />

<br />

BA Flour<br />

0<br />

<br />

<br />

BA Grain<br />

0<br />

<br />

<br />

BA Meat<br />

0<br />

<br />

<br />

BA Microbiol<strong>og</strong>y<br />

0<br />

<br />

<br />

BA Milk and Dairy<br />

0<br />

<br />

<br />

BA Pet Food<br />

0<br />

<br />

<br />

BA Pharmaceutical<br />

0<br />

<br />

<br />

BA Sugar<br />

0<br />

<br />

<br />

Side 81


BA Water and Soil<br />

0<br />

<br />

<br />

BA Wine<br />

0<br />

<br />

<br />

NEK<br />

0<br />

true<br />

true<br />

<br />

<br />

<br />

<br />

1<br />

Contact Id<br />

true<br />

<br />

<br />

2<br />

Lead Id<br />

true<br />

<br />

<br />

3<br />

First Name<br />

true<br />

<br />

<br />

4<br />

Last Name<br />

true<br />

<br />

<br />

5<br />

Job Category<br />

true<br />

<br />

<br />

6<br />

Country<br />

true<br />

<br />

<br />

7<br />

EMAIL<br />

true<br />

<br />

<br />

8<br />

Company Id<br />

true<br />

<br />

<br />

9<br />

Company Name<br />

true<br />

<br />

<br />

10<br />

RSM First Name<br />

true<br />

<br />

<br />

11<br />

RSM Last Name<br />

Side 82


true<br />

<br />

<br />

12<br />

RSM Job Title<br />

true<br />

<br />

<br />

13<br />

RSM Company<br />

true<br />

<br />

<br />

14<br />

RSM EMail<br />

true<br />

<br />

<br />

15<br />

RSM Phone<br />

true<br />

<br />

<br />

16<br />

BA Analytical Labs<br />

true<br />

<br />

<br />

17<br />

BA Beer<br />

true<br />

<br />

<br />

18<br />

BA Biofuel<br />

true<br />

<br />

<br />

19<br />

BA Chemicals<br />

true<br />

<br />

<br />

20<br />

BA Confectionery<br />

true<br />

<br />

<br />

21<br />

BA Dressings and Condiments<br />

true<br />

<br />

<br />

22<br />

BA Edible Oils and Fats<br />

true<br />

<br />

<br />

23<br />

BA Feed and Forage<br />

true<br />

<br />

<br />

24<br />

BA Flour<br />

true<br />

<br />

<br />

Side 83


25<br />

BA Grain<br />

true<br />

<br />

<br />

26<br />

BA Meat<br />

true<br />

<br />

<br />

27<br />

BA Microbiol<strong>og</strong>y<br />

true<br />

<br />

<br />

28<br />

BA Milk and Dairy<br />

true<br />

<br />

<br />

29<br />

BA Pet Food<br />

true<br />

<br />

<br />

30<br />

BA Pharmaceutical<br />

true<br />

<br />

<br />

31<br />

BA Sugar<br />

true<br />

<br />

<br />

32<br />

BA Water and Soil<br />

true<br />

<br />

<br />

33<br />

BA Wine<br />

true<br />

<br />

<br />

34<br />

NEK<br />

true<br />

<br />

<br />

<br />

Side 84

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!