Caution: You are browsing the legacy symfony 1.x part of this website.

symfony advent calendar day twelve: Emails

1.0

지난 줄거리

어제 우리는 askeet 어플리케이션이 질문들을 다른 미디어 - RSS 피드 - 를 통해 전파할 수 있도록 확장하였습니다. Symfony 는 단순히 웹 페이지만을 위해 만들어지지 않았고, 오늘의 튜토리얼에서도 이를 증명해보이겠습니다. 우리는 MVC 구조의 장점을 살려서 이메일을 보내도록 할것입니다.

비밀번호 복구

로그인 폼 들은 (모든 페이지에 있는 AJAX 로그인 폼과 상단 메뉴에서 연결되는 기존 방식의 로그인 폼) 사용자 이름과 비밀번호를 입력해야만 하지만 사용자들은 너무 자주 이들을 까먹습니다. 우리는 이런 경우를 위해 그들이 접속할 수 있는 방법을 제공해야 합니다.

우리는 비밀번호를 평문으로 저장하고 있지 않기 때문에, 비밀번호를 임의의 비밀번호로 재설정하고 그것을 사용자에게 이메일로 보내는 방식을 선택하겠습니다. 현재는 사용자가 자신의 비밀번호를 변경할 수가 없기 때문에, 임의의 비밀번호는 기억하기 매우 어려울 것입니다. 이는 추후에 논의하도록 하겠습니다.

비밀번호 요청 폼

user 모듈에 이메일 주소를 입력할 수 있는 폼을 출력할 새로운 액션을 만들겠습니다. askeet/apps/frontend/modules/user/actions/action.class.php 파일에 다음을 추가합니다.

public function executePasswordRequest()
{
}

modules/user/templates/ 폴더에 passwordRequestSuccess.php 파일을 만듭니다.

<h2>Receive your login details by email</h2>
<p>Did you forget your password? Enter your email to receive your login details:</p>
<?php echo form_tag('@user_require_password') ?>
  <?php echo form_error('email') ?>
  <label for="email">email:</label>
  <?php echo input_tag('email', $sf_params->get('email'), 'style=width:150px') ?><br />
  <?php echo submit_tag('Send') ?>
</form>

이 폼은 로그인 폼들에서 연결이 되어야 하기 때문에, layout.phploginSuccess.php 에 다음을 추가합니다.

<?php echo link_to('Forgot your password?', '@user_require_password') ?>

routing.yml 에 비밀번호 관련 규칙을 추가합니다.

user_require_password:
  url:   /password_request
  param: { module: user, action: passwordRequest }

폼 값 확인

먼저 폼이 제출되었을때의 폼 값 확인 규칙을 살펴보겠습니다. modules/user/validate/ 디렉토리에 passwordRequest.yml 파일을 만듭니다.

methods:
  post:            [email]

names:
  email:
    required:      Yes
    required_msg:  You must provide an email
    validators:    emailValidator

emailValidator:
    class:         sfEmailValidator
    param:
      email_error: 'You didn''t enter a valid email address (for example: [email protected]). Please try again.'

그리고, askeet/apps/frontend/modules/user/actions/actions.class.php 에 다음 메쏘드를 추가하여, 에러가 발생한 경우에는 passwordRequest 폼을 에러메시지와 함께 다시 보여주도록 합니다.

public function handleErrorPasswordRequest()
{
  return sfView::SUCCESS;
}

요청 처리하기

우리는 6일째 에 있는 것과 같은 액션을 사용할 것입니다. 해당 액션에 다음을 추가합니다.

public function executePasswordRequest()
{
  if ($this->getRequest()->getMethod() != sfRequest::POST)
  {
    // display the form
    return sfView::SUCCESS;
  }
 
  // handle the form submission
  $c = new Criteria();
  $c->add(UserPeer::EMAIL, $this->getRequestParameter('email'));
  $user = UserPeer::doSelectOne($c);
 
  // email exists?
  if ($user)
  {
    // set new random password
    $password = substr(md5(rand(100000, 999999)), 0, 6);
    $user->setPassword($password);
 
    $this->getRequest()->setAttribute('password', $password);
    $this->getRequest()->setAttribute('nickname', $user->getNickname());
 
    $raw_email = $this->sendEmail('mail', 'sendPassword');
    $this->logMessage($raw_email, 'debug');
 
    // save new password
    $user->save();
 
    return 'MailSent';
  }
  else
  {
    $this->getRequest()->setError('email', 'There is no askeet user with this email address. Please try again');
 
    return sfView::SUCCESS;
  }
}

만약 사용자가 존재한다면, 위 액션은 임의의 비밀번호를 생성합니다. 그리고 다른 액션을 (mail/sendPassword) 호출하고 결과를 $raw_email 에 저장합니다. sfAction 클래스의 ->sendEmail() 메쏘드는 ->forward() 의 변형입니다. ->forward() 메쏘드가 실행되면 기존의 액션은 중지되지만, 이 메쏘드는 호출된 메쏘드가 실행된 이후에 원래의 액션으로 돌아옵니다. 덧붙여서, 이 메쏘드는 로그 파일에 기록할 수 있는 메일 메시지 원본을 반환 합니다 (로그파일에 정보를 기록하는 방법은 온라인 문서중 디버그 부분 을 살펴보시기 바랍니다).

만약 이메일이 성공적으로 발송되었다면, 위 액션은 'MailSent' 를 반환함으로써, 기본 템플릿인 passwordRequestSuccess.php 대신 passwordRequestMailSent.php 템플릿을 출력하도록 지시합니다.

참고: 우리가 6일째 했던 방식을 따라 했다면, 이메일 주소가 존재하는지에 대한 확인은 사용자 폼값 확인 클래스에서 이뤄졌어야 합니다. 하지만 아시다시피 "세상에는 한 가지 방식만 있는 것이 아닙니다". ->setError() 메쏘드를 사용함으로써, 우리는 중복된 데이터베이스 질의를 피할 수 있고, 훨씬 긴 폼값 확인 클래스를 만들지 않아도 됩니다.

메일이 발송되었음을 확인해주는 passwordRequestMailSent.php 템플릿을 만듭니다.

<h2>Confirmation - login information sent</h2>
 
<p>Your login information was sent to</p>
<p><?php echo $sf_params->get('email') ?></p>
<p>You should receive it shortly, so you can proceed to 
the <?php echo link_to('login page', '@login') ?>.</p>

이메일 발송

좋습니다. 이제 사용자가 올바른 이메일 주소를 입력할 경우, mail/sendPassword 액션이 실행될 것입니다. 해당 액션을 만들어보겠습니다.

이메일 전송 액션

mail 모듈을 만듭니다.

$ symfony init-module frontend mail

sendPassword 액션을 추가합니다.

public function executeSendPassword()
{
  $mail = new sfMail();
  $mail->addAddress($this->getRequestParameter('email'));
  $mail->setFrom('Askeet <[email protected]>');
  $mail->setSubject('Askeet password recovery');
 
  $mail->setPriority(1);
 
  $mail->addEmbeddedImage(sfConfig::get('sf_web_dir').'/legacy/images/askeet_logo.gif', 'CID1', 'Askeet Logo', 'base64', 'image/gif');
 
  $this->mail = $mail;
 
  $this->nickname = $this->getRequest()->getAttribute('nickname');
  $this->password = $this->getRequest()->getAttribute('password');
}

이 액션은 메일 발송을 담당하는 sfMail 객체를 사용합니다. 이메일 헤더들은 이 액션에서 모두 정의됩니다만, 본문의 경우에는 일반 텍스트 대신 좀 더 복잡한 내용을 사용하기 위해 템플릿을 사용할 것입니다 (그렇지 않을 경우 ->setBody() 메쏘드를 사용하실 수 있습니다).

첨부 이미지는 ->addEmbeddedImage() 메쏘드를 서버상의 이미지 경로, 템플릿 상에서의 아이디, 그리고 대체 텍스트와 파일 형식과 함께 호출함으로써 추가할 수 있습니다.

참고: sfMail 을 사용해서 첨부파일 역시 쉽게 처리할 수 있습니다.:

// 바이너리 파일 첨부
$mail->addAttachment(sfConfig::get('sf_data_dir').'/MyDocument.doc');
// 텍스트 파일 첨부
$mail->addStringAttachment('this is some cool text to embed', 'file.txt');

sfMail 에 관한 좀 더 자세한 내용은 온라인 문서들 중 메일 부분 에서 찾으실 수 있습니다.

메일 템플릿

위 액션이 실행된 후에는 sendPasswordSuccess.php 파일을 사용하여 뷰가 구성됩니다. 이 템플릿이 이메일의 본문으로 사용될 것입니다.

<p>Dear askeet user,</p>
 
<p>A request for <?php echo $mail->getSubject() ?> was sent to this address.</p>
 
<p>For safety reasons, the askeet website does not store passwords in clear.
When you forget your password, askeet creates a new one that can be used in place.</p>
 
<p>You can now connect to your askeet profile with:</p>
 
<p>
nickname: <strong><?php echo $nickname ?></strong><br/>
password: <strong><?php echo $password ?></strong>
</p>
 
<p>To get connected, go to the <?php echo link_to('login page', '@login') ?>
and enter these codes.</p>
 
<p>We hope to see you soon on <img src="cid:CID1" /></p>
 
<p>The askeet email robot</p>    

다른 템플릿들과 마찬가지로, 이메일 템플릿에서도 헬퍼 함수들은 (여기서 쓰인 link_to() 헬퍼 함수같은) 잘 동작합니다. 또한 이메일을 멋지게 보이게 하기 위한 어떠한 HTML 도 사용이 가능합니다.

첨부 이미지는, 액션에서 이미지를 첨부할때 지정한 값을 cid: 뒤에 붙여줌으로써 사용이 가능합니다.

메일 본문 대체 텍스트

만약 sendPasswordSuccess.altbody.php 템플릿 파일을 미리 저장해 둔다면, 이 파일은 이메일의 대체 텍스트로 사용됩니다. 이 대체 텍스트는 HTML 을 표시하지 않는 클라이언트에게 표시됩니다.

Dear askeet user,
 
A request for <?php echo $mail->getSubject() ?> was sent to this address.
 
For safety reasons, the askeet website does not store passwords in clear.
When you forget your password, askeet creates a new one that can be used in place.
 
You can now connect to your askeet profile with:
 
nickname: <?php echo $nickname ?>
password: <?php echo $password ?>
 
To get connected, go to the login page (http://www.askeet.com/login)
and enter these codes.
 
We hope to see you soon on askeet!
 
The askeet email robot

설정

sfMail 의 출력관련 설정은 액션에서 정의되지만, 기타 다른 설정도 가능합니다. mailer.yml 파일을 생성하고 다음을 입력합니다.

dev:
  deliver:    off

all:
  mailer:     sendmail

이 설정을 통해 메일 발송시 사용할 프로그램을 지정하고, 개발환경에서는 메일을 보내지 않도록 하고 있습니다 (테스트 데이터의 이메일들은 가짜긴 하지만).

여러분은 사용자가 이 메일링 액션에 접근하는 것을 원치 않을 것입니다. module.yml 파일을 모듈의 config/ 디렉토리에 만들고 다음을 입력합니다.

all:
  is_internal: on    

테스트

이제 새로운 비밀번호 복구 시스템을 테스트 해보겠습니다. 테스트 데이터에 여러분의 이메일을 사용한 새로운 사용자를 만들고 import_data.php 를 실행합니다.

캐시를 지우고 안정버전의 비밀번호 복구 페이지로 접속합니다. 여러분의 이메일을 입력하고 폼을 제출한 후, 이메일이 도착하는지 확인합니다.

이메일

내일 이 시간에

symfony 의 이메일 시스템은 간단하고 강력합니다. 간단한 이메일은 더할나위없이 간단하고, 복잡한 이메일도 HTML 을 작성하는 것보다 어렵지 않습니다. 이제 여러분의 다음 이메일 캠페인에는 상용 이메일 솔루션 대신 symfony 를 쓰시는게 좋을 것입니다...

어쨌든, 내일은 태그의 날입니다. Askeet 의 질문들에 태그를 달것이고, 이 태그들을 검색 가능하게 할 것이빈다. 그리고 여러분이 지금껏 상상해보지 못 한 멋진 태그 클라우드를 만들것입니다.

평소처럼, 오늘의 코드는 askeet SVN 저장소 에서 /tags/release_day_12 로 다운로드 가능합니다. 우리는 아직 21일째에 무엇을 할지 정하지 못 했습니다. askeet 메일링 리스트askeet 포럼 에 여러분의 의견을 남겨주시기 바랍니다.