Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ Otherwise, here's the quick guide for advanced users:
* Finally, point the document root of your web server to the `public` folder. That's where the index.php file is.


## Updating

Every time you update the files, don't forget to run `composer update` to update dependencies, specially for the framework, and also run `php config/boot.php db:migrate` to run database migrations (if any).
## Updating

Every time you update the files, don't forget to run `composer update` to update dependencies, specially for the framework, and also run `php config/boot.php db:migrate` to run database migrations (if any).

## Mail Namespace Migration Notes

MyImouto now treats `MyImouto\\Mail\\*` and `MyImouto\\Mime\\*` as the canonical runtime namespace for mail/message behavior.

`Zend\\Mail\\*` and `Zend\\Mime\\*` are compatibility shims only.

Shim removal criteria:
- no first-party code depends on `Zend\\*` internals beyond compatibility entrypoints.
- mail regression tests (password reset + dmail notification paths) pass on canonical classes.
- no unresolved external/runtime references to `Zend\\*` mail classes are observed during rollout.

Rollback guidance:
- keep `lib/Zend/*` wrappers in place for mixed-version deploy windows.
- if mail regressions occur after migration, roll back to previous release and keep shim wrappers enabled while restoring canonical behavior under `MyImouto\\*`.
82 changes: 82 additions & 0 deletions db/migrate/20260218101500_ensure_legacy_ip_addr_columns_exist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
class EnsureLegacyIpAddrColumnsExist extends Rails\ActiveRecord\Migration\Base
{
public function up()
{
$targets = [
['table' => 'dmails', 'column' => 'ip_addr', 'limit' => 46],
['table' => 'forum_posts', 'column' => 'ip_addr', 'limit' => 46],
['table' => 'forum_posts', 'column' => 'updater_ip_addr', 'limit' => 46],
['table' => 'comments', 'column' => 'updater_ip_addr', 'limit' => 46],
];

foreach ($targets as $target) {
$this->ensureNullableVarcharColumn($target['table'], $target['column'], $target['limit']);
}
}

private function ensureNullableVarcharColumn($tableName, $columnName, $limit)
{
if (!$this->tableExists($tableName)) {
return;
}

$metadata = $this->fetchColumnMetadata($tableName, $columnName);
if (!$metadata) {
$this->execute(
sprintf(
"ALTER TABLE `%s` ADD `%s` VARCHAR(%d) NULL",
$this->quoteIdentifier($tableName),
$this->quoteIdentifier($columnName),
(int)$limit
)
);
return;
}

if ($this->columnMatchesExpected($metadata, $limit)) {
return;
}

$this->execute(
sprintf(
"ALTER TABLE `%s` MODIFY `%s` VARCHAR(%d) NULL",
$this->quoteIdentifier($tableName),
$this->quoteIdentifier($columnName),
(int)$limit
)
);
}

private function fetchColumnMetadata($tableName, $columnName)
{
$stmt = $this->connection->executeSql(
"SELECT COLUMN_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1",
$tableName,
$columnName
);

$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return $row ?: null;
}

private function columnMatchesExpected(array $metadata, $limit)
{
$columnType = strtolower((string)$metadata['COLUMN_TYPE']);
$length = (int)$metadata['CHARACTER_MAXIMUM_LENGTH'];
$isNullable = strtoupper((string)$metadata['IS_NULLABLE']) === 'YES';

return strpos($columnType, 'varchar(') === 0
&& $length === (int)$limit
&& $isNullable;
}

private function quoteIdentifier($identifier)
{
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $identifier)) {
throw new \InvalidArgumentException('Invalid SQL identifier: ' . $identifier);
}

return str_replace('`', '``', $identifier);
}
}
35 changes: 35 additions & 0 deletions lib/MyImouto/Mail/Address.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace MyImouto\Mail;

class Address
{
private $email = '';
private $name = '';

public function __construct($email, $name = '')
{
$email = trim((string)$email);
$name = trim((string)$name);

if ($email === '') {
throw new \InvalidArgumentException('Email cannot be empty');
}
if (strpos($email, "\r") !== false || strpos($email, "\n") !== false) {
throw new \InvalidArgumentException('Email contains invalid characters');
}

$this->email = $email;
$this->name = $name;
}

public function getEmail()
{
return $this->email;
}

public function getName()
{
return $this->name;
}
}
29 changes: 29 additions & 0 deletions lib/MyImouto/Mail/Header/ContentType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace MyImouto\Mail\Header;

class ContentType
{
private $type = 'text/plain';

public function __construct($type = 'text/plain')
{
$this->setType($type);
}

public function setType($type)
{
$type = trim((string)$type);
if ($type === '') {
$type = 'text/plain';
}

$this->type = $type;
return $this;
}

public function getType()
{
return $this->type;
}
}
31 changes: 31 additions & 0 deletions lib/MyImouto/Mail/Header/GenericHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace MyImouto\Mail\Header;

class GenericHeader
{
private $name = '';
private $value = '';

public function __construct($name, $value = '')
{
$this->name = strtolower(trim((string)$name));
$this->setFieldValue($value);
}

public function setFieldValue($value)
{
$this->value = trim((string)$value);
return $this;
}

public function getFieldValue()
{
return $this->value;
}

public function getFieldName()
{
return $this->name;
}
}
50 changes: 50 additions & 0 deletions lib/MyImouto/Mail/Headers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace MyImouto\Mail;

use MyImouto\Mail\Header\ContentType;
use MyImouto\Mail\Header\GenericHeader;

class Headers
{
private $headers = [];

public function get($name)
{
$key = strtolower(trim((string)$name));
if ($key === '') {
throw new \InvalidArgumentException('Header name cannot be empty');
}

if (!isset($this->headers[$key])) {
$this->headers[$key] = $this->createHeader($key);
}

return $this->headers[$key];
}

public function set($name, $header)
{
$key = strtolower(trim((string)$name));
if ($key === '') {
throw new \InvalidArgumentException('Header name cannot be empty');
}

$this->headers[$key] = $header;
return $this;
}

public function toArray()
{
return $this->headers;
}

private function createHeader($name)
{
if ($name === 'content-type') {
return new ContentType();
}

return new GenericHeader($name);
}
}
Loading