From 2491be9809b2a83675a504a2149f3158df8ea6da Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:00:12 +0530 Subject: [PATCH] Step 2: Data models and migrations (Email, EmailBody, factories) --- app/Models/Email.php | 69 +++++++++++++++++ app/Models/EmailBody.php | 18 +++++ database/factories/EmailBodyFactory.php | 35 +++++++++ database/factories/EmailFactory.php | 77 +++++++++++++++++++ .../2026_03_05_081922_create_emails_table.php | 44 +++++++++++ routes/api.php | 3 + 6 files changed, 246 insertions(+) create mode 100644 app/Models/Email.php create mode 100644 app/Models/EmailBody.php create mode 100644 database/factories/EmailBodyFactory.php create mode 100644 database/factories/EmailFactory.php create mode 100644 database/migrations/2026_03_05_081922_create_emails_table.php create mode 100644 routes/api.php diff --git a/app/Models/Email.php b/app/Models/Email.php new file mode 100644 index 0000000..7a76d46 --- /dev/null +++ b/app/Models/Email.php @@ -0,0 +1,69 @@ + */ + use HasFactory; + + protected $table = 'emails'; + + protected $fillable = [ + 'unique_id_hash', + 'recipient_email', + 'recipient_name', + 'sender_email', + 'sender_name', + 'domain', + 'subject', + 'preview', + 'attachments_json', + 'attachment_size', + 'is_read', + 'received_at', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'attachments_json' => 'array', + 'attachment_size' => 'integer', + 'is_read' => 'boolean', + 'received_at' => 'datetime', + ]; + } + + /** + * Scope: filter emails by recipient address. + */ + public function scopeForRecipient(Builder $query, string $email): Builder + { + return $query->where('recipient_email', $email); + } + + /** + * Scope: filter emails by domain. + */ + public function scopeForDomain(Builder $query, string $domain): Builder + { + return $query->where('domain', $domain); + } + + /** + * Scope: filter unread emails only. + */ + public function scopeUnread(Builder $query): Builder + { + return $query->where('is_read', false); + } +} diff --git a/app/Models/EmailBody.php b/app/Models/EmailBody.php new file mode 100644 index 0000000..2d1f994 --- /dev/null +++ b/app/Models/EmailBody.php @@ -0,0 +1,18 @@ + $attributes + */ + public static function make(array $attributes = []): EmailBody + { + return new EmailBody(array_merge([ + 'unique_id_hash' => hash('sha256', fake()->uuid()), + 'body_text' => fake()->paragraphs(3, true), + 'body_html' => '

'.implode('

', fake()->paragraphs(3)).'

', + ], $attributes)); + } + + /** + * Create and persist a new EmailBody instance. + * + * @param array $attributes + */ + public static function create(array $attributes = []): EmailBody + { + $body = static::make($attributes); + $body->save(); + + return $body; + } +} diff --git a/database/factories/EmailFactory.php b/database/factories/EmailFactory.php new file mode 100644 index 0000000..5c4a718 --- /dev/null +++ b/database/factories/EmailFactory.php @@ -0,0 +1,77 @@ + + */ +class EmailFactory extends Factory +{ + protected $model = Email::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $domain = fake()->domainName(); + $bodyText = fake()->paragraphs(3, true); + + return [ + 'unique_id_hash' => hash('sha256', fake()->uuid()), + 'recipient_email' => fake()->userName().'@'.$domain, + 'recipient_name' => fake()->name(), + 'sender_email' => fake()->safeEmail(), + 'sender_name' => fake()->name(), + 'domain' => $domain, + 'subject' => fake()->sentence(6), + 'preview' => mb_substr($bodyText, 0, 500), + 'attachments_json' => [], + 'attachment_size' => 0, + 'is_read' => false, + 'received_at' => fake()->dateTimeBetween('-7 days', 'now'), + ]; + } + + /** + * Mark the email as read. + */ + public function read(): static + { + return $this->state(fn (array $attributes) => [ + 'is_read' => true, + ]); + } + + /** + * Add attachments to the email. + */ + public function withAttachments(int $count = 1): static + { + return $this->state(function (array $attributes) use ($count) { + $attachments = []; + $totalSize = 0; + + for ($i = 0; $i < $count; $i++) { + $size = fake()->numberBetween(1024, 5242880); + $totalSize += $size; + $attachments[] = [ + 'filename' => fake()->word().'.'.fake()->fileExtension(), + 'mimeType' => fake()->mimeType(), + 'size' => $size, + 's3_path' => 'mail-attachments/'.now()->format('Y/m/d').'/'.fake()->sha256().'_'.fake()->word().'.pdf', + ]; + } + + return [ + 'attachments_json' => $attachments, + 'attachment_size' => $totalSize, + ]; + }); + } +} diff --git a/database/migrations/2026_03_05_081922_create_emails_table.php b/database/migrations/2026_03_05_081922_create_emails_table.php new file mode 100644 index 0000000..600d7eb --- /dev/null +++ b/database/migrations/2026_03_05_081922_create_emails_table.php @@ -0,0 +1,44 @@ +id(); + $table->char('unique_id_hash', 64); + $table->string('recipient_email'); + $table->string('recipient_name')->default(''); + $table->string('sender_email'); + $table->string('sender_name')->default(''); + $table->string('domain'); + $table->string('subject', 500)->default(''); + $table->string('preview', 500)->default(''); + $table->json('attachments_json')->nullable(); + $table->unsignedBigInteger('attachment_size')->default(0); + $table->boolean('is_read')->default(false); + $table->timestamp('received_at')->nullable(); + $table->timestamps(); + + $table->unique('unique_id_hash', 'idx_unique_hash'); + $table->index('recipient_email', 'idx_recipient'); + $table->index('domain', 'idx_domain'); + $table->index('created_at', 'idx_created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('emails'); + } +}; diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..38075ee --- /dev/null +++ b/routes/api.php @@ -0,0 +1,3 @@ +