Mail’e Eklenmiş bir Dökümanın Okunup Okunmadığı Nasıl Anlaşılır ?
Selamlar,
Bu makalede oltalama amaçlı mail’e eklenmiş bir dökümanın, açılıp açılmadığını kontrol edeceğiz. Bir çok mail server, mail içine script konulmasına izin vermemektedir. Bu durum, bir dökümanın okunup okunmadığının anlaşılmasını, bir parça daha zorlaştırmakdadır.
Bu başlığı biraz daha genişletmek ister isek, Excel, Word, Pdf veya PowerPoint bir dökümanın, okunup okunmadığını nasıl anlarız diye değiştirebiliriz..
Bunun için döküman içerisine 1×1 pixel boyutunda bir resim konulmakta, ve “src” property’sine bir web servisi url’i atanmaktadır. Url içerisine parametre olarak, “documentID” ve “clientID”‘yi koymak yeterli olacaktır. Böylece ilgili dökümanın, kim tarafından okunduğu kaydedilebilecektir.
Gelin ilk önce bir dökümana 1×1 pixellik resim nasıl konur onu görelim :
Bu işi yapmanın birçok yolu vardır. Ama bu senaryoda amaç, office’in kurulu olmadığı bir makinada ya da Microsoft ürünlerinden bağımsız bu işin nasıl yapılabileceği üzerinedir?
Burada 3 önemli konu bulunmaktadır:
- Image url’nin dışardan, yani remote’dan verilebilmesi.
- Excel veya Pdf gibi istenen bir document tipinin belirlene bilmesi.
- İstenen boyutun, istenen birimde örneğin 1×1 pixel boyutunda verilebilmesi.
- Opsiyonel olarak, bu senaryo için gerekli olmasa da, resim tıklanınca redirect olunacak link de belirlenebilir.
Ben bu projede, 3th party kütüphane olarak GameBox.Spreadsheet kullandım. Ücretsiz kullanılabilse de, profesyonel kullanım için ücretli bir üründür. Ama performans, kalite ve bağımsızlık (Microsoft Office kurulmadan) için kullanılabilecek gördüğüm en kaliteli kütüphanelerden biridir.
Program.cs(Yeni Bir Excel): Aşağıdaki örnekde, sıfırdan bir Excel dosyası oluşturulmuş. İçine image gömülmüş ve “Pdf” olarak export edilip kaydedilmiştir. İstenir ise var olan bir Excel dosyası açılıp, ilgili image içine konabilir. Makelenin devamında bu örnek de yapılacaktır.
- “SpreadsheetInfo.SetLicense(“FREE-LIMITED-KEY”)“: Ücertsiz sürüm kullanılmıştır.
- “var workbook = new ExcelFile(); var worksheet = workbook.Worksheets.Add(“Images”)“: Yepyeni bir Excel dosyası oluşturulmuş ve “Images” adında bir worksheet yaratılmıştır.
- “worksheet.Cells[0, 0].Value = “Gerekli Dökümanlar:”;” : Excel’in, örnek amaçlı belli hücreleri doldurulmuştur.
- “worksheet.Cells.GetSubrangeAbsolute(0, 0, 0, 1).Merged = true;” : Yazılar bir cell’e sığmadığı için, belli sayıda column’u birleştirmek amaçlı kullanılmıştır.
- “var picture = worksheet.Pictures.Add(“https://www.borakasmer.com/wp-content/uploads/2022/09/istockphoto-898902746-612×612-1.jpeg”, “B9”, 720, 340, LengthUnit.Pixel);” : İlgili resim url’i, “borakasmer.com” üzerinden verilmiştir. Bu ilerde yazacağımız, web api yolu olarak değiştirilecektir. “B9” yazılacağı cell ve 720×340 boyutlarıdır. Siz bunu gerçek hayatta 1×1 şeklinde değiştirmeyi unutmayın.
- “picture.Hyperlink.Location = “https://www.borakasmer.com”; picture.Hyperlink.IsExternal = true;” : Resim tıklanınca gidilecek link belirlenmiştir. Bu senaryo için gerekli değildir. Sadece örnek amaçlı gösterilmiştir.
- “workbook.Save(@”C:\Projects\Images.pdf”);“: İlgili Excel, “pdf” olarak belirlenen klasöre kaydedilmiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using GemBox.Spreadsheet; using System; using System.Linq; namespace SaveImageToExcel { internal class Program { static void Main(string[] args) { SpreadsheetInfo.SetLicense("FREE-LIMITED-KEY"); var workbook = new ExcelFile(); var worksheet = workbook.Worksheets.Add("Images"); worksheet.Cells[0, 0].Value = "Gerekli Dökümanlar:"; worksheet.Cells.GetSubrangeAbsolute(0, 0, 0, 1).Merged = true; worksheet.Cells[0, 2].Value = "Diploma, Banka Hesap Defteri, Kimlik"; worksheet.Cells.GetSubrangeAbsolute(0, 2, 0, 6).Merged = true; worksheet.Cells[1, 0].Value = "Son başvuru tarihi:"; worksheet.Cells.GetSubrangeAbsolute(1, 0, 1, 1).Merged = true; worksheet.Cells[1, 2].Value = DateTime.Now.AddDays(7).ToShortDateString(); worksheet.Cells.GetSubrangeAbsolute(1, 2, 1, 5).Merged = true; worksheet.Cells[2, 0].Value = "Çin Mesaj:"; worksheet.Cells[2, 1].Value = new string(new char[] { '\u4f60', '\u597d' }); worksheet.Cells[4, 0].Value = "Gerekli işlemlerin zamanında yapılabilmesi için tüm istenen evrakların eksiksiz getirilmesi gerekmektedir."; worksheet.Cells.GetSubrangeAbsolute(4, 0, 4, 10).Merged = true; var picture = worksheet.Pictures.Add("https://www.borakasmer.com/wp-content/uploads/2022/09/istockphoto-898902746-612x612-1.jpeg", "B9", 720, 340, LengthUnit.Pixel); picture.Hyperlink.Location = "https://www.borakasmer.com"; picture.Hyperlink.IsExternal = true; workbook.Save(@"C:\Projects\Images.pdf"); } } } |
Images.pdf: Oluşturulan örnek pdf dosyası, aşağıdaki gibidir. Resmin görülmesi amaçlı, 720×340 gerçek boyutlarında tanımlanmıştır. Gerçek senaryoda image boyutları, 1×1 olması gerekmektedir. Tıklandığında “borakasmer.com”‘a gitmektedir.
Var olan Excel bir dosyaya Image’ın eklenmesi :
Bu işlem “GameBox.Spreadsheet” kütüphanesi ile var olan bir Excel dosyası için yapılabilirken, var olan bir Pdf dosyası için yapılamamaktadır. Pdf dosyasının editlenebilmesi için, aşağıdaki “GemBox.Pdf” kütüphanesine ihtiyaç vardır. Bu örnekde sadece Excel editlenecektir.
Aşağıda var olan bir “Images.xlsx” dosyası görülmektedir.
Program.cs (Var olan Excel’e Resim Ekleme):
- “var workbook = ExcelFile.Load(@”C:\Projects\Images.xlsx”);“: Var olan bir Image dosyası, yüklenmiştir.
- “var worksheet = workbook.Worksheets.First(ws => ws.Name == “Images”);“: İlgili Worksheet ada göre bulunmuştur.
- “var picture = worksheet.Pictures.Add(“https://www.borakasmer.com/wp-content/uploads/2022/09/istockphoto-898902746-612×612-1.jpeg”, “B9”, 720, 340, LengthUnit.Pixel); picture.Hyperlink.Location = “https://www.borakasmer.com”; picture.Hyperlink.IsExternal = true;”: İlgili Excel içine eklenecek Resim dosyası, tanımlanmış ve konulmuştur.
- “workbook.Save(@”C:\Projects\Images.xlsx”);“: Açılan Excel dosyası tekrardan kaydedilmiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using GemBox.Spreadsheet; using System; using System.Linq; namespace SaveImageToExcel { internal class Program { static void Main(string[] args) { SpreadsheetInfo.SetLicense("FREE-LIMITED-KEY"); var workbook = ExcelFile.Load(@"C:\Projects\Images.xlsx"); var worksheet = workbook.Worksheets.First(ws => ws.Name == "Images"); var picture = worksheet.Pictures.Add("https://www.borakasmer.com/wp-content/uploads/2022/09/istockphoto-898902746-612x612-1.jpeg", "B9", 720, 340, LengthUnit.Pixel); picture.Hyperlink.Location = "https://www.borakasmer.com"; picture.Hyperlink.IsExternal = true; workbook.Save(@"C:\Projects\Images.xlsx"); } } } |
Images.xlsx: Oluşturulan, içinde image olan Excel dosyasının son hali.
Şimdi gelin, önce backend servisini WebApi ile yazıp, sonra exceldeki ilgili image url’ini değiştirelim.
Backend:
Öncelikle yeni bir WepbApi projesi, “DocumentRead” .Net 7.0 ile aşağıdaki gibi yaratılır.
1-) DAL Katmanı:
MsSql Server DB Documents:
- Users: Mailin gönderildiği clientlar.
- Attachment: Mail içine konulmuş döküman.
- DocumentStatistics: Mail içindeki dökümanın, hangi clientlar tarafından kaç kere açıldığını ve kaç kere tıklandığını gösteren rapor tablosu.
DB: Documents: Local VM bir makinada aşağıdaki Tablolar oluşturulur.
Aynı Solution altına, DAL adında yeni bir Class Library projesi yaratılır. Aşağıdaki kütüphaneler Nuget’den indirilir.
Ayrıca makinanızda “dotnet tool” global’de yüklü değil ise aşağıdaki gibi yüklenir:
1 |
dotnet tool install --global dotnet-ef |
Visual Studio/View/Terminal altında “DAL” projesinin folder’ına gelinir.
Aşağıdaki “scaffold” komutu ile Documents DB’sine ait tüm Entityler “DAL” klasörü altında otomatik oluşturulur.
1 2 3 4 |
dotnet ef dbcontext scaffold "Server=.;Database=Documents;Trusted_Connection=True;Encrypt =False" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context-dir "Entities\DbContexts" --no-pluralize -c DocumentContext -f |
2-) Service Katmanı:
IReadService.cs: Aşağıdaki interfacede “userID” parametresi ile yani hangi client’ın, “attachmentID” ile yani hangi specific dökümanın, “actionTypeID” ile yani ne işlem yapıldığı (açtı ya da tıkladı) kaydı, tutulmaktadır.
1 2 3 4 5 6 7 |
namespace DocumentService { public interface IReadService { public Task<int> IncrementDocumentOpenedCountAsync(int userID, int attachmentID, int actionTypeID); } } |
ReadService.cs:
- DBContext DocumentContext’i, Dependency Injection ile constructor’da alınır.
- Asenkron dışarıya açık tek bir end-point hazırlanacaktır.Öncelikle bu user’ın ilgili döküman için, bir kaydı var mı diye bakılır.
- Eğer kayıt var ise, gelen actionTypeID parametresine göre client’ın ilgili döküman için yaptığı action seçilir. İlgili dökümanın ilgili client tarafından açılma ya da tıklanma sayısı, “count” değişkenine 1 arttırılarak atanır. Ve DBContext, yeni okunma ya da tıklanma sayısı ile güncellenir.
- Eğer önceden bu client’ın bu dökümana ait hiçbir kaydı yok ise, yani yeni tıklanma ya da okunma kaydı, DocumentStatistics entitysine eklenir ve _context kaydedilir. Son olarak toplam tıklanma ya da okunma sayısı geri dönülür.
ReadService.cs: Dökümana eklenen resmin, üzerinde yapılan işleme göre tıklanma ya da görüntülenme sayısını arttırıp, DB’ye kaydeden servis.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using DAL.Entities; using DAL.Entities.DbContexts; using Microsoft.EntityFrameworkCore; namespace DocumentService { public class ReadService : IReadService { DocumentContext _context; public ReadService(DocumentContext context) { _context = context; } public async Task<int> IncrementDocumentOpenedCountAsync(int userID, int attachmentID, int actionTypeID) { var updatedDocument = await _context.DocumentStatistics.FirstOrDefaultAsync(ds => ds.UserId == userID && ds.AttachmentId == attachmentID); var count = 0; if (updatedDocument != null) { if (actionTypeID == (int)DocumentType.Opened) { count = updatedDocument.OpenedCount + 1; updatedDocument.OpenedCount = count; } else if (actionTypeID == (int)DocumentType.Clicked) { count = updatedDocument.ClickCount + 1; updatedDocument.ClickCount = count; } await _context.SaveChangesAsync(); } else { var model = new DocumentStatistics(); model.UserId = userID; model.AttachmentId = attachmentID; model.OpenedCount = actionTypeID == (int)DocumentType.Opened ? 1 : 0; model.ClickCount = actionTypeID == (int)DocumentType.Clicked ? 1 : 0; _context.DocumentStatistics.Add(model); await _context.SaveChangesAsync(); } return count; } } } |
Enums: Dökümana yapılan işlem tipleri.
1 2 3 4 5 6 7 |
namespace DocumentService { public enum DocumentType { Opened = 1, Clicked = 2 } } |
3-) WebApi Endpoint Katmanı:
DocumentController.cs: Dışarıya açılan, dökümana eklenecek resmin yolu olarak gösterilecek endpoint, aşağıdaki gibidir.
- IRedisService Dependency Injection olarak constructor’da parametre olarak alınmıştır.
- [HttpGet] ile Resim, source’unu çeker. “DocumentRead“, Routing amaçlı ilgili controller’ın adıdır. Geri kalan “clientID”, “documentID” ve “actionTypeID” parametreleri sıra ile, mail atılan user, mail içinde açılan döküman ve son olarak client’ın yaptığı işlemdir.(open, click)
- “client” image’ın gerçek url’ine request çekilmek amaçlı kullanılan .Net “HttpClient” kütüphanesidir. Asenkron olarak gene bu blogdaki bir resim çekilir.
- Çekilen döküman ve client parametreleri ile, yukarıda yazılan servis çağrılır. İşlem tipine göre ya okunma ya da tıklanma sayısı arttırılır.
- Çekilen image ByteArray’e dönüştürülür ve Image’ın src’sine, File olarak geri dönülür.
DocumentController.cs: Kontrol edilecek dökümana eklenen resmin, kaynağının tanımlandığı EndPoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using DocumentService; using Microsoft.AspNetCore.Mvc; namespace DocumentRead.Controllers { [ApiController] [Route("[controller]")] public class DocumentController : ControllerBase { IReadService _service; public DocumentController(IReadService service) { _service = service; } [HttpGet(Name = "DocumentRead/{clientID}/{documentID}/{actionTypeID}")] public async Task<IActionResult> Get(int clientID, int documentID, int actionTypeID) { using (var client = new HttpClient()) using (HttpResponseMessage response = await client.GetAsync("https://www.borakasmer.com/wp-content/uploads/2022/09/istockphoto-898902746-612x612-1.jpeg")) { try { var count = await _service.IncrementDocumentOpenedCountAsync(clientID, documentID, actionTypeID); } catch(Exception ex) { Console.WriteLine(ex.Message); } byte[] fileContents = await response.Content.ReadAsByteArrayAsync(); return File(fileContents, "image/jpeg"); } } } } |
Program.cs: Aşağıda program.cs’e eklenmesi gereken iki satır gösterilmiştir. Biri Dependency Injection için yazılan IRedisService’in tanımlanması diğeri de DocumentContext’in connection string’i ile tanımlanmasıdır.
1 2 3 4 5 6 |
. . builder.Services.AddDbContext<DocumentContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddTransient<IReadService, ReadService>(); . . |
appsettings.json: İlgili Documents database’inin connection string’i aşağıdaki gibi tanımlanır.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "DefaultConnection": "Data Source=.;initial catalog=Documents;Trusted_Connection=True;" }, "AllowedHosts": "*" } |
launchSettings.json: Son olarak, ilgili .Net Core WebApi projesine dışardan erişimi için “applicationUrl” Ipsi “0.0.0.0” ve web protokolu olarak “Http” olarak ayarlanır.
Excel’e Eklenecek Image’ın Url’ine, Yukarıda Tanımlanan Endpoint’in Atanması
Excel Image Kod Güncelleme:
Program.cs(Excel Image Path Güncelleme): Aşağıda görüldüğü gibi Workseet’e eklenen Resim yolu değiştirilmiştir.
- “http://192.168.1.7:5094/”: WebApi servisinin yayımlandığı VM makinanın IP’sidir.
- “/Document”: WebApi’de tanımlanan Controller’ın adı. Routing amaçlı gidilecek endpoint’ın rootu belirtilmiş olunur.
- “clientID=1&documentID=2&actionTypeID=1”: Url’de WepApi’ye gönderilen parametreler. actionTypeID=1 => Okunma Action’ı dır.
- “&type=.jpg” : GemBox.Spreadsheet’de, Pictures’a atanan Url’in mutlaka geçerli bir uzantı ile bitmesi istenmektedir. Bu şekilde fake bir uzantı belirtilmiştir. WebServisine bu fake uzantı, sanki “type” parametresi şeklinde gönderilmektedir.
- “B9” Resmin Excel’de konumlanacağı hücreyi ifade etmektedir.
- “720,340” resmin boyutlarıdır. Normalde 1×1 yapılmalıdır. Ama bu örnekde ilgili resimin gözükmesi için bu boyutta verilmiştir.
- “LengthUnit.Pixel”: Resim boyut tipi enum olarak tanımlanmıştır.
- “ExcelObjectSourceType.Link“: Bu tanımlama yapılmaz ise, ilgili Excel’e resim en başta çekilip, Embeded olarak gömülmekte ve en başta yaratılırken ilgili endpoint’e gidip bir daha gitmemektedir. Bu tanımlama ile, Excel her açıldığın src için ilgili endpoint’e sorgu atmaktadır.
- “picture.Hyperlink.Location”: Image’ın tıklandığında gidilecek Url adresidir. Burada tek fark actionTypeID=2 => Tıklanma Action’ı dır.
- “picture.Hyperlink.IsExternal = true”: Dışarıya redirect edilen bir link olduğu tanımlanır.
- “workbook.Save(@”C:\Projects\Images.xlsx”)” : Tanımlanan path altına, “.xlsx” uzantısı ile ilgili Excel kaydedilir.
1 2 3 4 5 6 7 8 9 10 11 |
{ . . . var picture = worksheet.Pictures.Add("http://192.168.1.7:5094/Document?clientID=1&documentID=2&actionTypeID=1&type=.jpg", "B9", 720, 340, LengthUnit.Pixel, ExcelObjectSourceType.Link); picture.Hyperlink.Location = "http://192.168.1.7:5094/Document?clientID=1&documentID=2&actionTypeID=2"; picture.Hyperlink.IsExternal = true; workbook.Save(@"C:\Projects\Images.xlsx"); } |
Bu makalede, bir maile eklenen döküman ya da dökümanların okunup okunmadığını belirledik. Esas sorun, mail servislerinin script kodlara güvenlik nedeni ile izin vermemesi ve dökümanların okunup okunmamasının belirlenmesinin zor bir hal almasıdır. Yazılımda tabi ki yapılamıyacak iş çok azdır :) İlgili döküman içerisine görünmeyecek 1×1 pixellik bir image ve source dosyasına bizim belirlediğimiz custom bir endpoint’in verilmesi yeterlidir. İlgili döküman açıldığında, resim bizim url’e istekde bulunacak ve böylece hangi client’ın hangi dosyayı açtığı DB’ye kaydedilebilecektir. Burdaki asıl zorluk, excel veye diğer döküman tiplerinin oluşturulması sırasında bir başka Microsoft Ürününe ihtiyacın duyulmaması gerekliliğidir. Bu noktada “GameBox” kütüphaneleri imdadımıza yetişmektedir.
Geldik bir makalenin daha sonuna. Yeni bir maklede görüşmek üzere hoşçakalın.
Source Code: https://github.com/borakasmer/DetechedDocumentOpened
Source:
Hocam elinize sağlık ufuk açıcı.
Tesekkur ederim Ergun.